diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId')
224 files changed, 42452 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/ComponentModel/IdentifierConverter.cs b/src/DotNetOpenAuth.OpenId/ComponentModel/IdentifierConverter.cs new file mode 100644 index 0000000..61c0fd8 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/ComponentModel/IdentifierConverter.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="IdentifierConverter.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ComponentModel { + using System; + using System.Collections; + using System.ComponentModel.Design.Serialization; + using System.Reflection; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A design-time helper to give an OpenID Identifier property an auto-complete functionality + /// listing the OP Identifiers in the <see cref="WellKnownProviders"/> class. + /// </summary> + public class IdentifierConverter : ConverterBase<Identifier> { + /// <summary> + /// Initializes a new instance of the <see cref="IdentifierConverter"/> class. + /// </summary> + [Obsolete("This class is meant for design-time use within an IDE, and not meant to be used directly by runtime code.")] + public IdentifierConverter() { + } + + /// <summary> + /// Converts a value from its string representation to its strongly-typed object. + /// </summary> + /// <param name="value">The value.</param> + /// <returns>The strongly-typed object.</returns> + protected override Identifier ConvertFrom(string value) { + return value; + } + + /// <summary> + /// Creates the reflection instructions for recreating an instance later. + /// </summary> + /// <param name="value">The value to recreate later.</param> + /// <returns> + /// The description of how to recreate an instance. + /// </returns> + protected override InstanceDescriptor CreateFrom(Identifier value) { + if (value == null) { + return null; + } + + MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null); + return CreateInstanceDescriptor(identifierParse, new object[] { value.ToString() }); + } + + /// <summary> + /// Converts the strongly-typed value to a string. + /// </summary> + /// <param name="value">The value to convert.</param> + /// <returns>The string representation of the object.</returns> + protected override string ConvertToString(Identifier value) { + return value; + } + + /// <summary> + /// Gets the standard values to suggest with Intellisense in the designer. + /// </summary> + /// <returns>A collection of the standard values.</returns> + protected override ICollection GetStandardValuesForCache() { + return SuggestedStringsConverter.GetStandardValuesForCacheShared(typeof(WellKnownProviders)); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/AssociationTypeCollection.cs b/src/DotNetOpenAuth.OpenId/Configuration/AssociationTypeCollection.cs new file mode 100644 index 0000000..881fcdb --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/AssociationTypeCollection.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationTypeCollection.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.Contracts; + + /// <summary> + /// Describes a collection of association type sub-elements in a .config file. + /// </summary> + [ContractVerification(true)] + internal class AssociationTypeCollection : ConfigurationElementCollection, IEnumerable<AssociationTypeElement> { + /// <summary> + /// Initializes a new instance of the <see cref="AssociationTypeCollection"/> class. + /// </summary> + public AssociationTypeCollection() { + } + + #region IEnumerable<AssociationTypeElement> Members + + /// <summary> + /// Returns an enumerator that iterates through the collection. + /// </summary> + /// <returns> + /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection. + /// </returns> + public new IEnumerator<AssociationTypeElement> GetEnumerator() { + for (int i = 0; i < Count; i++) { + yield return (AssociationTypeElement)BaseGet(i); + } + } + + #endregion + + /// <summary> + /// When overridden in a derived class, creates a new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </summary> + /// <returns> + /// A new <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override ConfigurationElement CreateNewElement() { + return new AssociationTypeElement(); + } + + /// <summary> + /// Gets the element key for a specified configuration element when overridden in a derived class. + /// </summary> + /// <param name="element">The <see cref="T:System.Configuration.ConfigurationElement"/> to return the key for.</param> + /// <returns> + /// An <see cref="T:System.Object"/> that acts as the key for the specified <see cref="T:System.Configuration.ConfigurationElement"/>. + /// </returns> + protected override object GetElementKey(ConfigurationElement element) { + return ((AssociationTypeElement)element).AssociationType ?? string.Empty; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/AssociationTypeElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/AssociationTypeElement.cs new file mode 100644 index 0000000..0eaea0e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/AssociationTypeElement.cs @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationTypeElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// Describes an association type and its maximum lifetime as an element + /// in a .config file. + /// </summary> + [ContractVerification(true)] + internal class AssociationTypeElement : ConfigurationElement { + /// <summary> + /// The name of the attribute that stores the association type. + /// </summary> + private const string AssociationTypeConfigName = "type"; + + /// <summary> + /// The name of the attribute that stores the association's maximum lifetime. + /// </summary> + private const string MaximumLifetimeConfigName = "lifetime"; + + /// <summary> + /// Initializes a new instance of the <see cref="AssociationTypeElement"/> class. + /// </summary> + internal AssociationTypeElement() { + } + + /// <summary> + /// Gets or sets the protocol name of the association. + /// </summary> + [ConfigurationProperty(AssociationTypeConfigName, IsRequired = true, IsKey = true)] + ////[StringValidator(MinLength = 1)] + public string AssociationType { + get { return (string)this[AssociationTypeConfigName]; } + set { this[AssociationTypeConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum time a shared association should live. + /// </summary> + /// <value>The default value is 14 days.</value> + [ConfigurationProperty(MaximumLifetimeConfigName, IsRequired = true)] + public TimeSpan MaximumLifetime { + get { return (TimeSpan)this[MaximumLifetimeConfigName]; } + set { this[MaximumLifetimeConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdElement.cs new file mode 100644 index 0000000..69994e6 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdElement.cs @@ -0,0 +1,134 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Represents the <openid> element in the host's .config file. + /// </summary> + [ContractVerification(true)] + internal class OpenIdElement : ConfigurationElement { + /// <summary> + /// The name of the <relyingParty> sub-element. + /// </summary> + private const string RelyingPartyElementName = "relyingParty"; + + /// <summary> + /// The name of the <provider> sub-element. + /// </summary> + private const string ProviderElementName = "provider"; + + /// <summary> + /// The name of the <extensions> sub-element. + /// </summary> + private const string ExtensionFactoriesElementName = "extensionFactories"; + + /// <summary> + /// The name of the <xriResolver> sub-element. + /// </summary> + private const string XriResolverElementName = "xriResolver"; + + /// <summary> + /// The name of the @maxAuthenticationTime attribute. + /// </summary> + private const string MaxAuthenticationTimePropertyName = "maxAuthenticationTime"; + + /// <summary> + /// The name of the @cacheDiscovery attribute. + /// </summary> + private const string CacheDiscoveryPropertyName = "cacheDiscovery"; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdElement"/> class. + /// </summary> + internal OpenIdElement() { + } + + /// <summary> + /// Gets or sets the maximum time a user can take to complete authentication. + /// </summary> + /// <remarks> + /// This time limit allows the library to decide how long to cache certain values + /// necessary to complete authentication. The lower the time, the less demand on + /// the server. But too short a time can frustrate the user. + /// </remarks> + [ConfigurationProperty(MaxAuthenticationTimePropertyName, DefaultValue = "0:05")] // 5 minutes + [PositiveTimeSpanValidator] + internal TimeSpan MaxAuthenticationTime { + get { + Contract.Ensures(Contract.Result<TimeSpan>() > TimeSpan.Zero); + TimeSpan result = (TimeSpan)this[MaxAuthenticationTimePropertyName]; + Contract.Assume(result > TimeSpan.Zero); // our PositiveTimeSpanValidator should take care of this + return result; + } + + set { + Contract.Requires<ArgumentOutOfRangeException>(value > TimeSpan.Zero); + this[MaxAuthenticationTimePropertyName] = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether the results of Identifier discovery + /// should be cached. + /// </summary> + /// <value> + /// Use <c>true</c> to allow identifier discovery to immediately return cached results when available; + /// otherwise, use <c>false</c>.to force fresh results every time at the cost of slightly slower logins. + /// The default value is <c>true</c>. + /// </value> + /// <remarks> + /// When enabled, caching is done according to HTTP standards. + /// </remarks> + [ConfigurationProperty(CacheDiscoveryPropertyName, DefaultValue = true)] + internal bool CacheDiscovery { + get { return (bool)this[CacheDiscoveryPropertyName]; } + set { this[CacheDiscoveryPropertyName] = value; } + } + + /// <summary> + /// Gets or sets the configuration specific for Relying Parties. + /// </summary> + [ConfigurationProperty(RelyingPartyElementName)] + internal OpenIdRelyingPartyElement RelyingParty { + get { return (OpenIdRelyingPartyElement)this[RelyingPartyElementName] ?? new OpenIdRelyingPartyElement(); } + set { this[RelyingPartyElementName] = value; } + } + + /// <summary> + /// Gets or sets the configuration specific for Providers. + /// </summary> + [ConfigurationProperty(ProviderElementName)] + internal OpenIdProviderElement Provider { + get { return (OpenIdProviderElement)this[ProviderElementName] ?? new OpenIdProviderElement(); } + set { this[ProviderElementName] = value; } + } + + /// <summary> + /// Gets or sets the registered OpenID extension factories. + /// </summary> + [ConfigurationProperty(ExtensionFactoriesElementName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(TypeConfigurationCollection<IOpenIdExtensionFactory>))] + internal TypeConfigurationCollection<IOpenIdExtensionFactory> ExtensionFactories { + get { return (TypeConfigurationCollection<IOpenIdExtensionFactory>)this[ExtensionFactoriesElementName] ?? new TypeConfigurationCollection<IOpenIdExtensionFactory>(); } + set { this[ExtensionFactoriesElementName] = value; } + } + + /// <summary> + /// Gets or sets the configuration for the XRI resolver. + /// </summary> + [ConfigurationProperty(XriResolverElementName)] + internal XriResolverElement XriResolver { + get { return (XriResolverElement)this[XriResolverElementName] ?? new XriResolverElement(); } + set { this[XriResolverElementName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdProviderElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdProviderElement.cs new file mode 100644 index 0000000..a594e86 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdProviderElement.cs @@ -0,0 +1,67 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdProviderElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// The section in the .config file that allows customization of OpenID Provider behaviors. + /// </summary> + [ContractVerification(true)] + internal class OpenIdProviderElement : ConfigurationElement { + /// <summary> + /// The name of the security sub-element. + /// </summary> + private const string SecuritySettingsConfigName = "security"; + + /// <summary> + /// Gets the name of the <behaviors> sub-element. + /// </summary> + private const string BehaviorsElementName = "behaviors"; + + /// <summary> + /// The name of the custom store sub-element. + /// </summary> + private const string StoreConfigName = "store"; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProviderElement"/> class. + /// </summary> + public OpenIdProviderElement() { + } + + /// <summary> + /// Gets or sets the security settings. + /// </summary> + [ConfigurationProperty(SecuritySettingsConfigName)] + public OpenIdProviderSecuritySettingsElement SecuritySettings { + get { return (OpenIdProviderSecuritySettingsElement)this[SecuritySettingsConfigName] ?? new OpenIdProviderSecuritySettingsElement(); } + set { this[SecuritySettingsConfigName] = value; } + } + + /// <summary> + /// Gets or sets the special behaviors to apply. + /// </summary> + [ConfigurationProperty(BehaviorsElementName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(TypeConfigurationCollection<IProviderBehavior>))] + public TypeConfigurationCollection<IProviderBehavior> Behaviors { + get { return (TypeConfigurationCollection<IProviderBehavior>)this[BehaviorsElementName] ?? new TypeConfigurationCollection<IProviderBehavior>(); } + set { this[BehaviorsElementName] = value; } + } + + /// <summary> + /// Gets or sets the type to use for storing application state. + /// </summary> + [ConfigurationProperty(StoreConfigName)] + public TypeConfigurationElement<IOpenIdApplicationStore> ApplicationStore { + get { return (TypeConfigurationElement<IOpenIdApplicationStore>)this[StoreConfigName] ?? new TypeConfigurationElement<IOpenIdApplicationStore>(); } + set { this[StoreConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdProviderSecuritySettingsElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdProviderSecuritySettingsElement.cs new file mode 100644 index 0000000..0d8e8b4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdProviderSecuritySettingsElement.cs @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdProviderSecuritySettingsElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// Represents the .config file element that allows for setting the security policies of the Provider. + /// </summary> + [ContractVerification(true)] + internal class OpenIdProviderSecuritySettingsElement : ConfigurationElement { + /// <summary> + /// Gets the name of the @protectDownlevelReplayAttacks attribute. + /// </summary> + private const string ProtectDownlevelReplayAttacksConfigName = "protectDownlevelReplayAttacks"; + + /// <summary> + /// Gets the name of the @minimumHashBitLength attribute. + /// </summary> + private const string MinimumHashBitLengthConfigName = "minimumHashBitLength"; + + /// <summary> + /// Gets the name of the @maximumHashBitLength attribute. + /// </summary> + private const string MaximumHashBitLengthConfigName = "maximumHashBitLength"; + + /// <summary> + /// The name of the associations collection sub-element. + /// </summary> + private const string AssociationsConfigName = "associations"; + + /// <summary> + /// The name of the @encodeAssociationSecretsInHandles attribute. + /// </summary> + private const string EncodeAssociationSecretsInHandlesConfigName = "encodeAssociationSecretsInHandles"; + + /// <summary> + /// Gets the name of the @requireSsl attribute. + /// </summary> + private const string RequireSslConfigName = "requireSsl"; + + /// <summary> + /// Gets the name of the @unsolicitedAssertionVerification attribute. + /// </summary> + private const string UnsolicitedAssertionVerificationConfigName = "unsolicitedAssertionVerification"; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProviderSecuritySettingsElement"/> class. + /// </summary> + public OpenIdProviderSecuritySettingsElement() { + } + + /// <summary> + /// Gets or sets a value indicating whether all discovery and authentication should require SSL security. + /// </summary> + [ConfigurationProperty(RequireSslConfigName, DefaultValue = false)] + public bool RequireSsl { + get { return (bool)this[RequireSslConfigName]; } + set { this[RequireSslConfigName] = value; } + } + + /// <summary> + /// Gets or sets the minimum length of the hash that protects the protocol from hijackers. + /// </summary> + [ConfigurationProperty(MinimumHashBitLengthConfigName, DefaultValue = SecuritySettings.MinimumHashBitLengthDefault)] + public int MinimumHashBitLength { + get { return (int)this[MinimumHashBitLengthConfigName]; } + set { this[MinimumHashBitLengthConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum length of the hash that protects the protocol from hijackers. + /// </summary> + [ConfigurationProperty(MaximumHashBitLengthConfigName, DefaultValue = SecuritySettings.MaximumHashBitLengthRPDefault)] + public int MaximumHashBitLength { + get { return (int)this[MaximumHashBitLengthConfigName]; } + set { this[MaximumHashBitLengthConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether the Provider should take special care + /// to protect OpenID 1.x relying parties against replay attacks. + /// </summary> + [ConfigurationProperty(ProtectDownlevelReplayAttacksConfigName, DefaultValue = ProviderSecuritySettings.ProtectDownlevelReplayAttacksDefault)] + public bool ProtectDownlevelReplayAttacks { + get { return (bool)this[ProtectDownlevelReplayAttacksConfigName]; } + set { this[ProtectDownlevelReplayAttacksConfigName] = value; } + } + + /// <summary> + /// Gets or sets the level of verification a Provider performs on an identifier before + /// sending an unsolicited assertion for it. + /// </summary> + /// <value>The default value is <see cref="ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.RequireSuccess"/>.</value> + [ConfigurationProperty(UnsolicitedAssertionVerificationConfigName, DefaultValue = ProviderSecuritySettings.UnsolicitedAssertionVerificationDefault)] + public ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel UnsolicitedAssertionVerification { + get { return (ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel)this[UnsolicitedAssertionVerificationConfigName]; } + set { this[UnsolicitedAssertionVerificationConfigName] = value; } + } + + /// <summary> + /// Gets or sets the configured lifetimes of the various association types. + /// </summary> + [ConfigurationProperty(AssociationsConfigName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(AssociationTypeCollection))] + public AssociationTypeCollection AssociationLifetimes { + get { + Contract.Ensures(Contract.Result<AssociationTypeCollection>() != null); + return (AssociationTypeCollection)this[AssociationsConfigName] ?? new AssociationTypeCollection(); + } + + set { + this[AssociationsConfigName] = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether the Provider should ease the burden of storing associations + /// by encoding their secrets (in signed, encrypted form) into the association handles themselves, storing only + /// a few rotating, private symmetric keys in the Provider's store instead. + /// </summary> + [ConfigurationProperty(EncodeAssociationSecretsInHandlesConfigName, DefaultValue = ProviderSecuritySettings.EncodeAssociationSecretsInHandlesDefault)] + public bool EncodeAssociationSecretsInHandles { + get { return (bool)this[EncodeAssociationSecretsInHandlesConfigName]; } + set { this[EncodeAssociationSecretsInHandlesConfigName] = value; } + } + + /// <summary> + /// Initializes a programmatically manipulatable bag of these security settings with the settings from the config file. + /// </summary> + /// <returns>The newly created security settings object.</returns> + public ProviderSecuritySettings CreateSecuritySettings() { + ProviderSecuritySettings settings = new ProviderSecuritySettings(); + settings.RequireSsl = this.RequireSsl; + settings.MinimumHashBitLength = this.MinimumHashBitLength; + settings.MaximumHashBitLength = this.MaximumHashBitLength; + settings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks; + settings.UnsolicitedAssertionVerification = this.UnsolicitedAssertionVerification; + settings.EncodeAssociationSecretsInHandles = this.EncodeAssociationSecretsInHandles; + foreach (AssociationTypeElement element in this.AssociationLifetimes) { + Contract.Assume(element != null); + settings.AssociationLifetimes.Add(element.AssociationType, element.MaximumLifetime); + } + + return settings; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs new file mode 100644 index 0000000..17b890f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdRelyingPartyElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// The section in the .config file that allows customization of OpenID Relying Party behaviors. + /// </summary> + [ContractVerification(true)] + internal class OpenIdRelyingPartyElement : ConfigurationElement { + /// <summary> + /// The name of the custom store sub-element. + /// </summary> + private const string StoreConfigName = "store"; + + /// <summary> + /// The name of the attribute that specifies whether dnoa.userSuppliedIdentifier is tacked onto the openid.return_to URL. + /// </summary> + private const string PreserveUserSuppliedIdentifierConfigName = "preserveUserSuppliedIdentifier"; + + /// <summary> + /// Gets the name of the security sub-element. + /// </summary> + private const string SecuritySettingsConfigName = "security"; + + /// <summary> + /// The name of the <behaviors> sub-element. + /// </summary> + private const string BehaviorsElementName = "behaviors"; + + /// <summary> + /// The name of the <discoveryServices> sub-element. + /// </summary> + private const string DiscoveryServicesElementName = "discoveryServices"; + + /// <summary> + /// The built-in set of identifier discovery services. + /// </summary> + private static readonly TypeConfigurationCollection<IIdentifierDiscoveryService> defaultDiscoveryServices = new TypeConfigurationCollection<IIdentifierDiscoveryService>(new Type[] { typeof(UriDiscoveryService), typeof(XriDiscoveryProxyService) }); + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingPartyElement"/> class. + /// </summary> + public OpenIdRelyingPartyElement() { + } + + /// <summary> + /// Gets or sets a value indicating whether "dnoa.userSuppliedIdentifier" is tacked onto the openid.return_to URL in order to preserve what the user typed into the OpenID box. + /// </summary> + /// <value> + /// The default value is <c>true</c>. + /// </value> + [ConfigurationProperty(PreserveUserSuppliedIdentifierConfigName, DefaultValue = true)] + public bool PreserveUserSuppliedIdentifier { + get { return (bool)this[PreserveUserSuppliedIdentifierConfigName]; } + set { this[PreserveUserSuppliedIdentifierConfigName] = value; } + } + + /// <summary> + /// Gets or sets the security settings. + /// </summary> + [ConfigurationProperty(SecuritySettingsConfigName)] + public OpenIdRelyingPartySecuritySettingsElement SecuritySettings { + get { return (OpenIdRelyingPartySecuritySettingsElement)this[SecuritySettingsConfigName] ?? new OpenIdRelyingPartySecuritySettingsElement(); } + set { this[SecuritySettingsConfigName] = value; } + } + + /// <summary> + /// Gets or sets the special behaviors to apply. + /// </summary> + [ConfigurationProperty(BehaviorsElementName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(TypeConfigurationCollection<IRelyingPartyBehavior>))] + public TypeConfigurationCollection<IRelyingPartyBehavior> Behaviors { + get { return (TypeConfigurationCollection<IRelyingPartyBehavior>)this[BehaviorsElementName] ?? new TypeConfigurationCollection<IRelyingPartyBehavior>(); } + set { this[BehaviorsElementName] = value; } + } + + /// <summary> + /// Gets or sets the type to use for storing application state. + /// </summary> + [ConfigurationProperty(StoreConfigName)] + public TypeConfigurationElement<IOpenIdApplicationStore> ApplicationStore { + get { return (TypeConfigurationElement<IOpenIdApplicationStore>)this[StoreConfigName] ?? new TypeConfigurationElement<IOpenIdApplicationStore>(); } + set { this[StoreConfigName] = value; } + } + + /// <summary> + /// Gets or sets the services to use for discovering service endpoints for identifiers. + /// </summary> + /// <remarks> + /// If no discovery services are defined in the (web) application's .config file, + /// the default set of discovery services built into the library are used. + /// </remarks> + [ConfigurationProperty(DiscoveryServicesElementName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(TypeConfigurationCollection<IIdentifierDiscoveryService>))] + internal TypeConfigurationCollection<IIdentifierDiscoveryService> DiscoveryServices { + get { + var configResult = (TypeConfigurationCollection<IIdentifierDiscoveryService>)this[DiscoveryServicesElementName]; + return configResult != null && configResult.Count > 0 ? configResult : defaultDiscoveryServices; + } + + set { + this[DiscoveryServicesElementName] = value; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs new file mode 100644 index 0000000..e116f52 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs @@ -0,0 +1,266 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdRelyingPartySecuritySettingsElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Represents the .config file element that allows for setting the security policies of the Relying Party. + /// </summary> + internal class OpenIdRelyingPartySecuritySettingsElement : ConfigurationElement { + /// <summary> + /// Gets the name of the @minimumRequiredOpenIdVersion attribute. + /// </summary> + private const string MinimumRequiredOpenIdVersionConfigName = "minimumRequiredOpenIdVersion"; + + /// <summary> + /// Gets the name of the @minimumHashBitLength attribute. + /// </summary> + private const string MinimumHashBitLengthConfigName = "minimumHashBitLength"; + + /// <summary> + /// Gets the name of the @maximumHashBitLength attribute. + /// </summary> + private const string MaximumHashBitLengthConfigName = "maximumHashBitLength"; + + /// <summary> + /// Gets the name of the @requireSsl attribute. + /// </summary> + private const string RequireSslConfigName = "requireSsl"; + + /// <summary> + /// Gets the name of the @requireDirectedIdentity attribute. + /// </summary> + private const string RequireDirectedIdentityConfigName = "requireDirectedIdentity"; + + /// <summary> + /// Gets the name of the @requireAssociation attribute. + /// </summary> + private const string RequireAssociationConfigName = "requireAssociation"; + + /// <summary> + /// Gets the name of the @rejectUnsolicitedAssertions attribute. + /// </summary> + private const string RejectUnsolicitedAssertionsConfigName = "rejectUnsolicitedAssertions"; + + /// <summary> + /// Gets the name of the @rejectDelegatedIdentifiers attribute. + /// </summary> + private const string RejectDelegatingIdentifiersConfigName = "rejectDelegatingIdentifiers"; + + /// <summary> + /// Gets the name of the @ignoreUnsignedExtensions attribute. + /// </summary> + private const string IgnoreUnsignedExtensionsConfigName = "ignoreUnsignedExtensions"; + + /// <summary> + /// Gets the name of the @allowDualPurposeIdentifiers attribute. + /// </summary> + private const string AllowDualPurposeIdentifiersConfigName = "allowDualPurposeIdentifiers"; + + /// <summary> + /// Gets the name of the @allowApproximateIdentifierDiscovery attribute. + /// </summary> + private const string AllowApproximateIdentifierDiscoveryConfigName = "allowApproximateIdentifierDiscovery"; + + /// <summary> + /// Gets the name of the @protectDownlevelReplayAttacks attribute. + /// </summary> + private const string ProtectDownlevelReplayAttacksConfigName = "protectDownlevelReplayAttacks"; + + /// <summary> + /// The name of the <trustedProviders> sub-element. + /// </summary> + private const string TrustedProvidersElementName = "trustedProviders"; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingPartySecuritySettingsElement"/> class. + /// </summary> + public OpenIdRelyingPartySecuritySettingsElement() { + } + + /// <summary> + /// Gets or sets a value indicating whether all discovery and authentication should require SSL security. + /// </summary> + [ConfigurationProperty(RequireSslConfigName, DefaultValue = false)] + public bool RequireSsl { + get { return (bool)this[RequireSslConfigName]; } + set { this[RequireSslConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether only OP Identifiers will be discoverable + /// when creating authentication requests. + /// </summary> + [ConfigurationProperty(RequireDirectedIdentityConfigName, DefaultValue = false)] + public bool RequireDirectedIdentity { + get { return (bool)this[RequireDirectedIdentityConfigName]; } + set { this[RequireDirectedIdentityConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether authentication requests + /// will only be created where an association with the Provider can be established. + /// </summary> + [ConfigurationProperty(RequireAssociationConfigName, DefaultValue = false)] + public bool RequireAssociation { + get { return (bool)this[RequireAssociationConfigName]; } + set { this[RequireAssociationConfigName] = value; } + } + + /// <summary> + /// Gets or sets the minimum OpenID version a Provider is required to support in order for this library to interoperate with it. + /// </summary> + /// <remarks> + /// Although the earliest versions of OpenID are supported, for security reasons it may be desirable to require the + /// remote party to support a later version of OpenID. + /// </remarks> + [ConfigurationProperty(MinimumRequiredOpenIdVersionConfigName, DefaultValue = "V10")] + public ProtocolVersion MinimumRequiredOpenIdVersion { + get { return (ProtocolVersion)this[MinimumRequiredOpenIdVersionConfigName]; } + set { this[MinimumRequiredOpenIdVersionConfigName] = value; } + } + + /// <summary> + /// Gets or sets the minimum length of the hash that protects the protocol from hijackers. + /// </summary> + [ConfigurationProperty(MinimumHashBitLengthConfigName, DefaultValue = SecuritySettings.MinimumHashBitLengthDefault)] + public int MinimumHashBitLength { + get { return (int)this[MinimumHashBitLengthConfigName]; } + set { this[MinimumHashBitLengthConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum length of the hash that protects the protocol from hijackers. + /// </summary> + [ConfigurationProperty(MaximumHashBitLengthConfigName, DefaultValue = SecuritySettings.MaximumHashBitLengthRPDefault)] + public int MaximumHashBitLength { + get { return (int)this[MaximumHashBitLengthConfigName]; } + set { this[MaximumHashBitLengthConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether all unsolicited assertions should be ignored. + /// </summary> + /// <value>The default value is <c>false</c>.</value> + [ConfigurationProperty(RejectUnsolicitedAssertionsConfigName, DefaultValue = false)] + public bool RejectUnsolicitedAssertions { + get { return (bool)this[RejectUnsolicitedAssertionsConfigName]; } + set { this[RejectUnsolicitedAssertionsConfigName] = value; } + } + + /// <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> + [ConfigurationProperty(RejectDelegatingIdentifiersConfigName, DefaultValue = false)] + public bool RejectDelegatingIdentifiers { + get { return (bool)this[RejectDelegatingIdentifiersConfigName]; } + set { this[RejectDelegatingIdentifiersConfigName] = value; } + } + + /// <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> + [ConfigurationProperty(IgnoreUnsignedExtensionsConfigName, DefaultValue = false)] + public bool IgnoreUnsignedExtensions { + get { return (bool)this[IgnoreUnsignedExtensionsConfigName]; } + set { this[IgnoreUnsignedExtensionsConfigName] = value; } + } + + /// <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> + [ConfigurationProperty(AllowDualPurposeIdentifiersConfigName, DefaultValue = false)] + public bool AllowDualPurposeIdentifiers { + get { return (bool)this[AllowDualPurposeIdentifiersConfigName]; } + set { this[AllowDualPurposeIdentifiersConfigName] = value; } + } + + /// <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> + [ConfigurationProperty(AllowApproximateIdentifierDiscoveryConfigName, DefaultValue = true)] + public bool AllowApproximateIdentifierDiscovery { + get { return (bool)this[AllowApproximateIdentifierDiscoveryConfigName]; } + set { this[AllowApproximateIdentifierDiscoveryConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether the Relying Party should take special care + /// to protect users against replay attacks when interoperating with OpenID 1.1 Providers. + /// </summary> + [ConfigurationProperty(ProtectDownlevelReplayAttacksConfigName, DefaultValue = RelyingPartySecuritySettings.ProtectDownlevelReplayAttacksDefault)] + public bool ProtectDownlevelReplayAttacks { + get { return (bool)this[ProtectDownlevelReplayAttacksConfigName]; } + set { this[ProtectDownlevelReplayAttacksConfigName] = value; } + } + + /// <summary> + /// Gets or sets the set of trusted OpenID Provider Endpoints. + /// </summary> + [ConfigurationProperty(TrustedProvidersElementName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(TrustedProviderConfigurationCollection))] + public TrustedProviderConfigurationCollection TrustedProviders { + get { return (TrustedProviderConfigurationCollection)this[TrustedProvidersElementName] ?? new TrustedProviderConfigurationCollection(); } + set { this[TrustedProvidersElementName] = value; } + } + + /// <summary> + /// Initializes a programmatically manipulatable bag of these security settings with the settings from the config file. + /// </summary> + /// <returns>The newly created security settings object.</returns> + public RelyingPartySecuritySettings CreateSecuritySettings() { + Contract.Ensures(Contract.Result<RelyingPartySecuritySettings>() != null); + + RelyingPartySecuritySettings settings = new RelyingPartySecuritySettings(); + settings.RequireSsl = this.RequireSsl; + settings.RequireDirectedIdentity = this.RequireDirectedIdentity; + settings.RequireAssociation = this.RequireAssociation; + settings.MinimumRequiredOpenIdVersion = this.MinimumRequiredOpenIdVersion; + settings.MinimumHashBitLength = this.MinimumHashBitLength; + settings.MaximumHashBitLength = this.MaximumHashBitLength; + settings.PrivateSecretMaximumAge = DotNetOpenAuthSection.Configuration.Messaging.PrivateSecretMaximumAge; + settings.RejectUnsolicitedAssertions = this.RejectUnsolicitedAssertions; + settings.RejectDelegatingIdentifiers = this.RejectDelegatingIdentifiers; + settings.IgnoreUnsignedExtensions = this.IgnoreUnsignedExtensions; + settings.AllowDualPurposeIdentifiers = this.AllowDualPurposeIdentifiers; + settings.AllowApproximateIdentifierDiscovery = this.AllowApproximateIdentifierDiscovery; + settings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks; + + settings.RejectAssertionsFromUntrustedProviders = this.TrustedProviders.RejectAssertionsFromUntrustedProviders; + foreach (TrustedProviderEndpointConfigurationElement opEndpoint in this.TrustedProviders) { + settings.TrustedProviderEndpoints.Add(opEndpoint.ProviderEndpoint); + } + + return settings; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Configuration/XriResolverElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/XriResolverElement.cs new file mode 100644 index 0000000..fdb5b29 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Configuration/XriResolverElement.cs @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------- +// <copyright file="XriResolverElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + + /// <summary> + /// Represents the <xriResolver> element in the host's .config file. + /// </summary> + internal class XriResolverElement : ConfigurationElement { + /// <summary> + /// Gets the name of the @enabled attribute. + /// </summary> + private const string EnabledAttributeName = "enabled"; + + /// <summary> + /// The default value for <see cref="Enabled"/>. + /// </summary> + private const bool EnabledDefaultValue = true; + + /// <summary> + /// The name of the <proxy> sub-element. + /// </summary> + private const string ProxyElementName = "proxy"; + + /// <summary> + /// The default XRI proxy resolver to use. + /// </summary> + private static readonly HostNameElement ProxyDefault = new HostNameElement("xri.net"); + + /// <summary> + /// Initializes a new instance of the <see cref="XriResolverElement"/> class. + /// </summary> + internal XriResolverElement() { + } + + /// <summary> + /// Gets or sets a value indicating whether this XRI resolution is enabled. + /// </summary> + /// <value>The default value is <c>true</c>.</value> + [ConfigurationProperty(EnabledAttributeName, DefaultValue = EnabledDefaultValue)] + internal bool Enabled { + get { return (bool)this[EnabledAttributeName]; } + set { this[EnabledAttributeName] = value; } + } + + /// <summary> + /// Gets or sets the proxy to use for resolving XRIs. + /// </summary> + /// <value>The default value is "xri.net".</value> + [ConfigurationProperty(ProxyElementName)] + internal HostNameElement Proxy { + get { + var host = (HostNameElement)this[ProxyElementName] ?? ProxyDefault; + return string.IsNullOrEmpty(host.Name.Trim()) ? ProxyDefault : host; + } + + set { + this[ProxyElementName] = value; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj b/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj new file mode 100644 index 0000000..c98092a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj @@ -0,0 +1,523 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))\EnlistmentInfo.props" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))' != '' " /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <CodeContractsAssemblyMode>1</CodeContractsAssemblyMode> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <PropertyGroup> + <ProductVersion>9.0.30729</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{3896A32A-E876-4C23-B9B8-78E17D134CD3}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth</RootNamespace> + <AssemblyName>DotNetOpenAuth.OpenId</AssemblyName> + <AssemblyName Condition=" '$(NoUIControls)' == 'true' ">DotNetOpenAuth.NoUI</AssemblyName> + <FileAlignment>512</FileAlignment> + <StandardCopyright> +Copyright (c) 2009, Andrew Arnott. All rights reserved. +Code licensed under the Ms-PL License: +http://opensource.org/licenses/ms-pl.html +</StandardCopyright> + <FileUpgradeFlags> + </FileUpgradeFlags> + <OldToolsVersion>3.5</OldToolsVersion> + <UpgradeBackupLocation /> + <IsWebBootstrapper>false</IsWebBootstrapper> + <TargetFrameworkProfile /> + <PublishUrl>publish\</PublishUrl> + <Install>true</Install> + <InstallFrom>Disk</InstallFrom> + <UpdateEnabled>false</UpdateEnabled> + <UpdateMode>Foreground</UpdateMode> + <UpdateInterval>7</UpdateInterval> + <UpdateIntervalUnits>Days</UpdateIntervalUnits> + <UpdatePeriodically>false</UpdatePeriodically> + <UpdateRequired>false</UpdateRequired> + <MapFileExtensions>true</MapFileExtensions> + <ApplicationRevision>0</ApplicationRevision> + <ApplicationVersion>1.0.0.%2a</ApplicationVersion> + <UseApplicationTrust>false</UseApplicationTrust> + <BootstrapperEnabled>true</BootstrapperEnabled> + <ApplicationIcon> + </ApplicationIcon> + <DocumentationFile>$(OutputPath)$(AssemblyName).xml</DocumentationFile> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>false</AllowUnsafeBlocks> + <RunCodeAnalysis>false</RunCodeAnalysis> + <CodeAnalysisRules> + </CodeAnalysisRules> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>Full</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>False</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>True</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>True</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>True</CodeContractsShowSquigglies> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + <CodeContractsExtraRewriteOptions /> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>false</AllowUnsafeBlocks> + <RunCodeAnalysis>true</RunCodeAnalysis> + <CodeAnalysisRules> + </CodeAnalysisRules> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>ReleaseRequires</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>False</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>False</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>False</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>False</CodeContractsShowSquigglies> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + <CodeContractsExtraRewriteOptions /> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'ReleaseNoUI|AnyCPU'"> + <DefineConstants>TRACE;NoUIControls</DefineConstants> + <NoUIControls>true</NoUIControls> + <Optimize>true</Optimize> + <NoWarn>;1607</NoWarn> + <DebugType>pdbonly</DebugType> + <PlatformTarget>AnyCPU</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>ReleaseRequires</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>False</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>False</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>False</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>False</CodeContractsShowSquigglies> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeContractsExtraRewriteOptions /> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeAnalysis|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DefineConstants>$(DefineConstants);CONTRACTS_FULL;DEBUG;TRACE</DefineConstants> + <DebugType>full</DebugType> + <PlatformTarget>AnyCPU</PlatformTarget> + <CodeAnalysisRules> + </CodeAnalysisRules> + <CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression> + <CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile> + <ErrorReport>prompt</ErrorReport> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>Preconditions</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>True</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>False</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>False</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>True</CodeContractsShowSquigglies> + <RunCodeAnalysis>true</RunCodeAnalysis> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + <CodeContractsExtraRewriteOptions /> + </PropertyGroup> + <ItemGroup> + <Reference Include="log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + </Reference> + <Reference Include="PresentationFramework"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Security" /> + <Reference Include="System.configuration" /> + <Reference Include="System.Core"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Data" /> + <Reference Include="System.Drawing" /> + <Reference Include="System.IdentityModel"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.IdentityModel.Selectors"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.Runtime.Serialization"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.ServiceModel"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.ServiceModel.Web"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web" /> + <Reference Include="System.Web.Abstractions"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web.Extensions"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web.Extensions.Design"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web.Mobile" Condition=" '$(ClrVersion)' != '4' " /> + <Reference Include="System.Web.Routing"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Windows.Forms" /> + <Reference Include="System.Xaml" Condition=" '$(ClrVersion)' == '4' " /> + <Reference Include="System.XML" /> + <Reference Include="System.Xml.Linq"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="WindowsBase"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.ComponentModel.DataAnnotations"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + </ItemGroup> + <ItemGroup Condition=" '$(ClrVersion)' == '4' "> + <Reference Include="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + </ItemGroup> + <ItemGroup Condition=" '$(ClrVersion)' != '4' "> + <!-- MVC 2 can run on CLR 2 (it doesn't require CLR 4) but since MVC 2 apps tend to use type forwarding, + it's a more broadly consumable idea to bind against MVC 1 for the library unless we're building on CLR 4, + which will definitely have MVC 2 available. --> + <Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + </ItemGroup> + <ItemGroup> + <Compile Include="ComponentModel\IdentifierConverter.cs" /> + <Compile Include="Configuration\AssociationTypeCollection.cs" /> + <Compile Include="Configuration\AssociationTypeElement.cs" /> + <Compile Include="Configuration\OpenIdElement.cs" /> + <Compile Include="Configuration\OpenIdProviderElement.cs" /> + <Compile Include="Configuration\OpenIdProviderSecuritySettingsElement.cs" /> + <Compile Include="Configuration\OpenIdRelyingPartyElement.cs" /> + <Compile Include="Configuration\OpenIdRelyingPartySecuritySettingsElement.cs" /> + <Compile Include="Configuration\XriResolverElement.cs" /> + <Compile Include="Mvc\OpenIdAjaxOptions.cs" /> + <Compile Include="Mvc\OpenIdHelper.cs" /> + <Compile Include="OpenId\Association.cs" /> + <Compile Include="OpenId\Provider\AssociationDataBag.cs" /> + <Compile Include="OpenId\Provider\IProviderAssociationStore.cs" /> + <Compile Include="OpenId\Provider\ProviderAssociationHandleEncoder.cs" /> + <Compile Include="OpenId\Provider\ProviderAssociationKeyStorage.cs" /> + <Compile Include="OpenId\RelyingParty\CryptoKeyStoreAsRelyingPartyAssociationStore.cs" /> + <Compile Include="OpenId\RelyingParty\IRelyingPartyAssociationStore.cs" /> + <Compile Include="OpenId\RelyingParty\Associations.cs" /> + <Compile Include="OpenId\Behaviors\AXFetchAsSregTransform.cs" /> + <Compile Include="OpenId\Behaviors\BehaviorStrings.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>BehaviorStrings.resx</DependentUpon> + </Compile> + <Compile Include="OpenId\Behaviors\PpidGeneration.cs" /> + <Compile Include="OpenId\ChannelElements\BackwardCompatibilityBindingElement.cs" /> + <Compile Include="OpenId\ChannelElements\ExtensionsBindingElement.cs" /> + <Compile Include="OpenId\ChannelElements\IOpenIdExtensionFactory.cs" /> + <Compile Include="OpenId\ChannelElements\ITamperResistantOpenIdMessage.cs" /> + <Compile Include="OpenId\ChannelElements\OriginalStringUriEncoder.cs" /> + <Compile Include="OpenId\ChannelElements\RelyingPartySecurityOptions.cs" /> + <Compile Include="OpenId\ChannelElements\ReturnToNonceBindingElement.cs" /> + <Compile Include="OpenId\ChannelElements\SigningBindingElement.cs" /> + <Compile Include="OpenId\ChannelElements\KeyValueFormEncoding.cs" /> + <Compile Include="OpenId\ChannelElements\OpenIdChannel.cs" /> + <Compile Include="OpenId\ChannelElements\OpenIdMessageFactory.cs" /> + <Compile Include="OpenId\ChannelElements\ReturnToSignatureBindingElement.cs" /> + <Compile Include="OpenId\ChannelElements\SkipSecurityBindingElement.cs" /> + <Compile Include="OpenId\AssociationContract.cs" /> + <Compile Include="OpenId\Extensions\AliasManager.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\AttributeRequest.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\AttributeValues.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\AXAttributeFormats.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\AXUtilities.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\Constants.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\FetchRequest.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\FetchResponse.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\StoreRequest.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\StoreResponse.cs" /> + <Compile Include="OpenId\Extensions\AttributeExchange\WellKnownAttributes.cs" /> + <Compile Include="OpenId\Extensions\ExtensionBase.cs" /> + <Compile Include="OpenId\Extensions\ExtensionArgumentsManager.cs" /> + <Compile Include="OpenId\Extensions\IClientScriptExtensionResponse.cs" /> + <Compile Include="OpenId\Extensions\OAuth\AuthorizationRequest.cs" /> + <Compile Include="OpenId\Extensions\OAuth\AuthorizationApprovedResponse.cs" /> + <Compile Include="OpenId\Extensions\OAuth\Constants.cs" /> + <Compile Include="OpenId\Extensions\OAuth\AuthorizationDeclinedResponse.cs" /> + <Compile Include="OpenId\Extensions\OpenIdExtensionFactoryAggregator.cs" /> + <Compile Include="OpenId\Extensions\StandardOpenIdExtensionFactory.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\AuthenticationPolicies.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\Constants.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\DateTimeEncoder.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\NistAssuranceLevel.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\PapeUtilities.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\PolicyRequest.cs" /> + <Compile Include="OpenId\Extensions\ProviderAuthenticationPolicy\PolicyResponse.cs" /> + <Compile Include="OpenId\Extensions\SimpleRegistration\ClaimsRequest.cs" /> + <Compile Include="OpenId\Extensions\SimpleRegistration\ClaimsResponse.cs" /> + <Compile Include="OpenId\Extensions\SimpleRegistration\Constants.cs" /> + <Compile Include="OpenId\Extensions\SimpleRegistration\DemandLevel.cs" /> + <Compile Include="OpenId\Extensions\SimpleRegistration\Gender.cs" /> + <Compile Include="OpenId\Extensions\UI\UIConstants.cs" /> + <Compile Include="OpenId\Extensions\UI\UIUtilities.cs" /> + <Compile Include="OpenId\Extensions\UI\UIModes.cs" /> + <Compile Include="OpenId\Extensions\UI\UIRequest.cs" /> + <Compile Include="OpenId\HostMetaDiscoveryService.cs" /> + <Compile Include="OpenId\Identifier.cs" /> + <Compile Include="OpenId\IdentifierContract.cs" /> + <Compile Include="OpenId\Extensions\ExtensionsInteropHelper.cs" /> + <Compile Include="OpenId\Interop\AuthenticationResponseShim.cs" /> + <Compile Include="OpenId\Interop\ClaimsResponseShim.cs" /> + <Compile Include="OpenId\Interop\OpenIdRelyingPartyShim.cs" /> + <Compile Include="OpenId\IIdentifierDiscoveryService.cs" /> + <Compile Include="OpenId\Messages\CheckAuthenticationRequest.cs" /> + <Compile Include="OpenId\Messages\CheckAuthenticationResponse.cs" /> + <Compile Include="OpenId\Messages\CheckIdRequest.cs" /> + <Compile Include="OpenId\Messages\AssociateSuccessfulResponseContract.cs" /> + <Compile Include="OpenId\Messages\IErrorMessage.cs" /> + <Compile Include="OpenId\Messages\IndirectResponseBase.cs" /> + <Compile Include="OpenId\Messages\IndirectSignedResponse.cs" /> + <Compile Include="OpenId\Messages\IOpenIdMessageExtension.cs" /> + <Compile Include="OpenId\Messages\NegativeAssertionResponse.cs" /> + <Compile Include="OpenId\Messages\PositiveAssertionResponse.cs" /> + <Compile Include="OpenId\Messages\SignedResponseRequest.cs" /> + <Compile Include="OpenId\NoDiscoveryIdentifier.cs" /> + <Compile Include="OpenId\OpenIdUtilities.cs" /> + <Compile Include="OpenId\Provider\AssociationRelyingPartyType.cs" /> + <Compile Include="OpenId\Provider\PrivatePersonalIdentifierProviderBase.cs" /> + <Compile Include="OpenId\Provider\AnonymousRequest.cs" /> + <Compile Include="OpenId\Provider\AnonymousRequestEventArgs.cs" /> + <Compile Include="OpenId\Provider\AuthenticationChallengeEventArgs.cs" /> + <Compile Include="OpenId\Provider\AuthenticationRequest.cs" /> + <Compile Include="OpenId\Provider\AutoResponsiveRequest.cs" /> + <Compile Include="OpenId\Provider\HostProcessedRequest.cs" /> + <Compile Include="OpenId\Provider\IAnonymousRequest.cs" /> + <Compile Include="OpenId\Provider\IAuthenticationRequest.cs" /> + <Compile Include="OpenId\Provider\IDirectedIdentityIdentifierProvider.cs" /> + <Compile Include="OpenId\Provider\IHostProcessedRequest.cs" /> + <Compile Include="OpenId\Provider\IErrorReporting.cs" /> + <Compile Include="OpenId\Provider\IProviderBehavior.cs" /> + <Compile Include="OpenId\Provider\IRequest.cs" /> + <Compile Include="OpenId\Provider\ProviderEndpoint.cs" /> + <Compile Include="OpenId\Provider\RelyingPartyDiscoveryResult.cs" /> + <Compile Include="OpenId\Provider\Request.cs" /> + <Compile Include="OpenId\Provider\RequestContract.cs" /> + <Compile Include="OpenId\Provider\StandardProviderApplicationStore.cs" /> + <Compile Include="OpenId\Realm.cs" /> + <Compile Include="OpenId\RelyingPartyDescription.cs" /> + <Compile Include="OpenId\DiffieHellmanUtilities.cs" /> + <Compile Include="OpenId\DiffieHellman\DHKeyGeneration.cs" /> + <Compile Include="OpenId\DiffieHellman\DHParameters.cs" /> + <Compile Include="OpenId\DiffieHellman\DiffieHellman.cs" /> + <Compile Include="OpenId\DiffieHellman\DiffieHellmanManaged.cs" /> + <Compile Include="OpenId\DiffieHellman\mono\BigInteger.cs" /> + <Compile Include="OpenId\DiffieHellman\mono\ConfidenceFactor.cs" /> + <Compile Include="OpenId\DiffieHellman\mono\NextPrimeFinder.cs" /> + <Compile Include="OpenId\DiffieHellman\mono\PrimalityTests.cs" /> + <Compile Include="OpenId\DiffieHellman\mono\PrimeGeneratorBase.cs" /> + <Compile Include="OpenId\DiffieHellman\mono\SequentialSearchPrimeGeneratorBase.cs" /> + <Compile Include="OpenId\HmacShaAssociation.cs" /> + <Compile Include="OpenId\Messages\AssociateUnencryptedRequest.cs" /> + <Compile Include="OpenId\Provider\OpenIdProvider.cs" /> + <Compile Include="OpenId\Messages\AssociateDiffieHellmanRequest.cs" /> + <Compile Include="OpenId\Messages\AssociateDiffieHellmanResponse.cs" /> + <Compile Include="OpenId\Messages\AssociateRequest.cs" /> + <Compile Include="OpenId\Messages\AssociateSuccessfulResponse.cs" /> + <Compile Include="OpenId\Messages\AssociateUnencryptedResponse.cs" /> + <Compile Include="OpenId\Messages\AssociateUnsuccessfulResponse.cs" /> + <Compile Include="OpenId\Messages\IndirectErrorResponse.cs" /> + <Compile Include="OpenId\Messages\DirectErrorResponse.cs" /> + <Compile Include="OpenId\Messages\RequestBase.cs" /> + <Compile Include="OpenId\Messages\DirectResponseBase.cs" /> + <Compile Include="OpenId\RelyingParty\AssociationManager.cs" /> + <Compile Include="OpenId\RelyingParty\AssociationPreference.cs" /> + <Compile Include="OpenId\RelyingParty\AuthenticationRequest.cs" /> + <Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" /> + <Compile Include="OpenId\RelyingParty\DuplicateRequestedHostsComparer.cs" /> + <Compile Include="OpenId\RelyingParty\IProviderEndpoint.cs" /> + <Compile Include="OpenId\RelyingParty\IRelyingPartyBehavior.cs" /> + <Compile Include="OpenId\RelyingParty\IAuthenticationRequestContract.cs" /> + <Compile Include="OpenId\RelyingParty\NegativeAuthenticationResponse.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdEventArgs.cs" /> + <Compile Include="OpenId\RelyingParty\PopupBehavior.cs" /> + <Compile Include="OpenId\RelyingParty\PositiveAnonymousResponse.cs" /> + <Compile Include="OpenId\RelyingParty\PositiveAuthenticationResponse.cs" /> + <Compile Include="OpenId\RelyingParty\AuthenticationStatus.cs" /> + <Compile Include="OpenId\RelyingParty\FailedAuthenticationResponse.cs" /> + <Compile Include="OpenId\RelyingParty\IAuthenticationRequest.cs" /> + <Compile Include="OpenId\RelyingParty\IAuthenticationResponse.cs" /> + <Compile Include="OpenId\RelyingParty\ISetupRequiredAuthenticationResponse.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdRelyingParty.cs" /> + <Compile Include="OpenId\OpenIdStrings.Designer.cs"> + <DependentUpon>OpenIdStrings.resx</DependentUpon> + <DesignTime>True</DesignTime> + <AutoGen>True</AutoGen> + </Compile> + <Compile Include="OpenId\Protocol.cs" /> + <Compile Include="OpenId\ProviderEndpointDescription.cs" /> + <Compile Include="OpenId\Provider\ProviderSecuritySettings.cs" /> + <Compile Include="OpenId\IOpenIdApplicationStore.cs" /> + <Compile Include="OpenId\RelyingParty\PositiveAuthenticationResponseSnapshot.cs" /> + <Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" /> + <Compile Include="OpenId\IdentifierDiscoveryResult.cs" /> + <Compile Include="OpenId\OpenIdXrdsHelper.cs" /> + <Compile Include="OpenId\RelyingParty\SimpleXrdsProviderEndpoint.cs" /> + <Compile Include="OpenId\RelyingParty\StandardRelyingPartyApplicationStore.cs" /> + <Compile Include="OpenId\Behaviors\GsaIcamProfile.cs" /> + <Compile Include="OpenId\RelyingParty\WellKnownProviders.cs" /> + <Compile Include="OpenId\SecuritySettings.cs" /> + <Compile Include="OpenId\UriDiscoveryService.cs" /> + <Compile Include="OpenId\UriIdentifier.cs" /> + <Compile Include="OpenId\XriDiscoveryProxyService.cs" /> + <Compile Include="OpenId\XriIdentifier.cs" /> + <Compile Include="XrdsPublisher.cs" /> + <Compile Include="Xrds\XrdsStrings.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>XrdsStrings.resx</DependentUpon> + </Compile> + <Compile Include="Yadis\ContentTypes.cs" /> + <Compile Include="Yadis\DiscoveryResult.cs" /> + <Compile Include="Yadis\HtmlParser.cs" /> + <Compile Include="Xrds\ServiceElement.cs" /> + <Compile Include="Xrds\TypeElement.cs" /> + <Compile Include="Xrds\UriElement.cs" /> + <Compile Include="Xrds\XrdElement.cs" /> + <Compile Include="Xrds\XrdsDocument.cs" /> + <Compile Include="Xrds\XrdsNode.cs" /> + <Compile Include="Yadis\Yadis.cs" /> + <EmbeddedResource Include="Xrds\XrdsStrings.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>XrdsStrings.Designer.cs</LastGenOutput> + </EmbeddedResource> + <EmbeddedResource Include="Xrds\XrdsStrings.sr.resx" /> + </ItemGroup> + <ItemGroup> + <BootstrapperPackage Include="Microsoft.Net.Client.3.5"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName> + <Install>false</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1"> + <Visible>False</Visible> + <ProductName>Windows Installer 3.1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + </ItemGroup> + <ItemGroup> + <SignDependsOn Include="BuildUnifiedProduct" /> + <DelaySignedAssemblies Include="$(ILMergeOutputAssembly);
 $(OutputPath)CodeContracts\$(ProductName).Contracts.dll;
 " /> + </ItemGroup> + <ItemGroup /> + <PropertyGroup> + <!-- Don't sign the non-unified version of the assembly. --> + <SuppressTargetPathDelaySignedAssembly>true</SuppressTargetPathDelaySignedAssembly> + </PropertyGroup> + <Target Name="BuildUnifiedProduct" DependsOnTargets="Build" Inputs="@(ILMergeInputAssemblies)" Outputs="$(ILMergeOutputAssembly)"> + <PropertyGroup> + <!-- The ILMerge task doesn't properly quote the path. --> + <ILMergeTargetPlatformDirectory Condition=" '$(ClrVersion)' == '4' ">"$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0"</ILMergeTargetPlatformDirectory> + </PropertyGroup> + <MakeDir Directories="$(ILMergeOutputAssemblyDirectory)" /> + <ILMerge ExcludeFile="$(ProjectRoot)ILMergeInternalizeExceptions.txt" InputAssemblies="@(ILMergeInputAssemblies)" OutputFile="$(ILMergeOutputAssembly)" KeyFile="$(PublicKeyFile)" DelaySign="true" ToolPath="$(ProjectRoot)tools\ILMerge" TargetPlatformVersion="$(ClrVersion).0" TargetPlatformDirectory="$(ILMergeTargetPlatformDirectory)" /> + </Target> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))\EnlistmentInfo.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))' != '' " /> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/Mvc/OpenIdAjaxOptions.cs b/src/DotNetOpenAuth.OpenId/Mvc/OpenIdAjaxOptions.cs new file mode 100644 index 0000000..4b88d04 --- /dev/null +++ b/src/DotNetOpenAuth.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/Mvc/OpenIdHelper.cs b/src/DotNetOpenAuth.OpenId/Mvc/OpenIdHelper.cs new file mode 100644 index 0000000..adde6b6 --- /dev/null +++ b/src/DotNetOpenAuth.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(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Configuration.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/OpenId/Association.cs b/src/DotNetOpenAuth.OpenId/OpenId/Association.cs new file mode 100644 index 0000000..5b97ad4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Association.cs @@ -0,0 +1,308 @@ +//----------------------------------------------------------------------- +// <copyright file="Association.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.IO; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Stores a secret used in signing and verifying messages. + /// </summary> + /// <remarks> + /// OpenID associations may be shared between Provider and Relying Party (smart + /// associations), or be a way for a Provider to recall its own secret for later + /// (dumb associations). + /// </remarks> + [DebuggerDisplay("Handle = {Handle}, Expires = {Expires}")] + [ContractVerification(true)] + [ContractClass(typeof(AssociationContract))] + public abstract class Association { + /// <summary> + /// Initializes a new instance of the <see cref="Association"/> class. + /// </summary> + /// <param name="handle">The handle.</param> + /// <param name="secret">The secret.</param> + /// <param name="totalLifeLength">How long the association will be useful.</param> + /// <param name="issued">The UTC time of when this association was originally issued by the Provider.</param> + protected Association(string handle, byte[] secret, TimeSpan totalLifeLength, DateTime issued) { + Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentNullException>(secret != null); + Contract.Requires<ArgumentOutOfRangeException>(totalLifeLength > TimeSpan.Zero); + Contract.Requires<ArgumentException>(issued.Kind == DateTimeKind.Utc); + Contract.Requires<ArgumentOutOfRangeException>(issued <= DateTime.UtcNow); + Contract.Ensures(this.TotalLifeLength == totalLifeLength); + + this.Handle = handle; + this.SecretKey = secret; + this.TotalLifeLength = totalLifeLength; + this.Issued = OpenIdUtilities.CutToSecond(issued); + } + + /// <summary> + /// Gets a unique handle by which this <see cref="Association"/> may be stored or retrieved. + /// </summary> + public string Handle { get; internal set; } + + /// <summary> + /// Gets the UTC time when this <see cref="Association"/> will expire. + /// </summary> + public DateTime Expires { + get { return this.Issued + this.TotalLifeLength; } + } + + /// <summary> + /// Gets a value indicating whether this <see cref="Association"/> has already expired. + /// </summary> + public bool IsExpired { + get { return this.Expires < DateTime.UtcNow; } + } + + /// <summary> + /// Gets the length (in bits) of the hash this association creates when signing. + /// </summary> + public abstract int HashBitLength { get; } + + /// <summary> + /// Gets a value indicating whether this instance has useful life remaining. + /// </summary> + /// <value> + /// <c>true</c> if this instance has useful life remaining; otherwise, <c>false</c>. + /// </value> + internal bool HasUsefulLifeRemaining { + get { return this.TimeTillExpiration >= MinimumUsefulAssociationLifetime; } + } + + /// <summary> + /// Gets or sets the UTC time that this <see cref="Association"/> was first created. + /// </summary> + [MessagePart] + internal DateTime Issued { get; set; } + + /// <summary> + /// Gets the number of seconds until this <see cref="Association"/> expires. + /// Never negative (counter runs to zero). + /// </summary> + protected internal long SecondsTillExpiration { + get { + Contract.Ensures(Contract.Result<long>() >= 0); + return Math.Max(0, (long)this.TimeTillExpiration.TotalSeconds); + } + } + + /// <summary> + /// Gets the shared secret key between the consumer and provider. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It is a buffer.")] + [MessagePart("key")] + protected internal byte[] SecretKey { get; private set; } + + /// <summary> + /// Gets the duration a secret key used for signing dumb client requests will be good for. + /// </summary> + protected static TimeSpan DumbSecretLifetime { + get { + Contract.Ensures(Contract.Result<TimeSpan>() > TimeSpan.Zero); + return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; + } + } + + /// <summary> + /// Gets the lifetime the OpenID provider permits this <see cref="Association"/>. + /// </summary> + [MessagePart("ttl")] + protected TimeSpan TotalLifeLength { get; private set; } + + /// <summary> + /// Gets the minimum lifetime an association must still be good for in order for it to be used for a future authentication. + /// </summary> + /// <remarks> + /// Associations that are not likely to last the duration of a user login are not worth using at all. + /// </remarks> + private static TimeSpan MinimumUsefulAssociationLifetime { + get { + Contract.Ensures(Contract.Result<TimeSpan>() > TimeSpan.Zero); + return DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime; + } + } + + /// <summary> + /// Gets the TimeSpan till this association expires. + /// </summary> + private TimeSpan TimeTillExpiration { + get { return this.Expires - DateTime.UtcNow; } + } + + /// <summary> + /// Re-instantiates an <see cref="Association"/> previously persisted in a database or some + /// other shared store. + /// </summary> + /// <param name="handle"> + /// The <see cref="Handle"/> property of the previous <see cref="Association"/> instance. + /// </param> + /// <param name="expiresUtc"> + /// The UTC value of the <see cref="Expires"/> property of the previous <see cref="Association"/> instance. + /// </param> + /// <param name="privateData"> + /// The byte array returned by a call to <see cref="SerializePrivateData"/> on the previous + /// <see cref="Association"/> instance. + /// </param> + /// <returns> + /// The newly dehydrated <see cref="Association"/>, which can be returned + /// from a custom association store's + /// <see cref="IRelyingPartyAssociationStore.GetAssociation(Uri, SecuritySettings)"/> method. + /// </returns> + public static Association Deserialize(string handle, DateTime expiresUtc, byte[] privateData) { + Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentNullException>(privateData != null); + Contract.Ensures(Contract.Result<Association>() != null); + + expiresUtc = expiresUtc.ToUniversalTimeSafe(); + TimeSpan remainingLifeLength = expiresUtc - DateTime.UtcNow; + byte[] secret = privateData; // the whole of privateData is the secret key for now. + // We figure out what derived type to instantiate based on the length of the secret. + try { + return HmacShaAssociation.Create(handle, secret, remainingLifeLength); + } catch (ArgumentException ex) { + throw new ArgumentException(OpenIdStrings.BadAssociationPrivateData, "privateData", ex); + } + } + + /// <summary> + /// Returns private data required to persist this <see cref="Association"/> in + /// permanent storage (a shared database for example) for deserialization later. + /// </summary> + /// <returns> + /// An opaque byte array that must be stored and returned exactly as it is provided here. + /// The byte array may vary in length depending on the specific type of <see cref="Association"/>, + /// but in current versions are no larger than 256 bytes. + /// </returns> + /// <remarks> + /// Values of public properties on the base class <see cref="Association"/> are not included + /// in this byte array, as they are useful for fast database lookup and are persisted separately. + /// </remarks> + public byte[] SerializePrivateData() { + Contract.Ensures(Contract.Result<byte[]>() != null); + + // We may want to encrypt this secret using the machine.config private key, + // and add data regarding which Association derivative will need to be + // re-instantiated on deserialization. + // For now, we just send out the secret key. We can derive the type from the length later. + byte[] secretKeyCopy = new byte[this.SecretKey.Length]; + if (this.SecretKey.Length > 0) { + this.SecretKey.CopyTo(secretKeyCopy, 0); + } + return secretKeyCopy; + } + + /// <summary> + /// Tests equality of two <see cref="Association"/> objects. + /// </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> + public override bool Equals(object obj) { + Association a = obj as Association; + if (a == null) { + return false; + } + if (a.GetType() != GetType()) { + return false; + } + + if (a.Handle != this.Handle || + a.Issued != this.Issued || + !MessagingUtilities.Equals(a.TotalLifeLength, this.TotalLifeLength, TimeSpan.FromSeconds(1))) { + return false; + } + + if (!MessagingUtilities.AreEquivalent(a.SecretKey, this.SecretKey)) { + return false; + } + + return true; + } + + /// <summary> + /// Returns the hash code. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + HMACSHA1 hmac = new HMACSHA1(this.SecretKey); + try { + CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); + + byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); + + cs.Write(hbytes, 0, hbytes.Length); + cs.Close(); + + byte[] hash = hmac.Hash; + hmac.Clear(); + + long val = 0; + for (int i = 0; i < hash.Length; i++) { + val = val ^ (long)hash[i]; + } + + val = val ^ this.Expires.ToFileTimeUtc(); + + return (int)val; + } finally { + ((IDisposable)hmac).Dispose(); + } + } + + /// <summary> + /// The string to pass as the assoc_type value in the OpenID protocol. + /// </summary> + /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param> + /// <returns>The value that should be used for the openid.assoc_type parameter.</returns> + internal abstract string GetAssociationType(Protocol protocol); + + /// <summary> + /// Generates a signature from a given blob of data. + /// </summary> + /// <param name="data">The data to sign. This data will not be changed (the signature is the return value).</param> + /// <returns>The calculated signature of the data.</returns> + protected internal byte[] Sign(byte[] data) { + Contract.Requires<ArgumentNullException>(data != null); + using (HashAlgorithm hasher = this.CreateHasher()) { + return hasher.ComputeHash(data); + } + } + + /// <summary> + /// Returns the specific hash algorithm used for message signing. + /// </summary> + /// <returns>The hash algorithm used for message signing.</returns> + protected abstract HashAlgorithm CreateHasher(); + +#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(!string.IsNullOrEmpty(this.Handle)); + Contract.Invariant(this.TotalLifeLength > TimeSpan.Zero); + Contract.Invariant(this.SecretKey != null); + } +#endif + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/AssociationContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/AssociationContract.cs new file mode 100644 index 0000000..57f4fd9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/AssociationContract.cs @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationContract.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.IO; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Code contract for the <see cref="Association"/> class. + /// </summary> + [ContractClassFor(typeof(Association))] + internal abstract class AssociationContract : Association { + /// <summary> + /// Prevents a default instance of the <see cref="AssociationContract"/> class from being created. + /// </summary> + private AssociationContract() + : base(null, null, TimeSpan.Zero, DateTime.Now) { + } + + /// <summary> + /// Gets the length (in bits) of the hash this association creates when signing. + /// </summary> + public override int HashBitLength { + get { + Contract.Ensures(Contract.Result<int>() > 0); + throw new NotImplementedException(); + } + } + + /// <summary> + /// The string to pass as the assoc_type value in the OpenID protocol. + /// </summary> + /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param> + /// <returns> + /// The value that should be used for the openid.assoc_type parameter. + /// </returns> + [Pure] + internal override string GetAssociationType(Protocol protocol) { + Contract.Requires<ArgumentNullException>(protocol != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Returns the specific hash algorithm used for message signing. + /// </summary> + /// <returns> + /// The hash algorithm used for message signing. + /// </returns> + [Pure] + protected override HashAlgorithm CreateHasher() { + Contract.Ensures(Contract.Result<HashAlgorithm>() != null); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/AXFetchAsSregTransform.cs b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/AXFetchAsSregTransform.cs new file mode 100644 index 0000000..01b74a1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/AXFetchAsSregTransform.cs @@ -0,0 +1,139 @@ +//----------------------------------------------------------------------- +// <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 AXFetchAsSregTransform : IRelyingPartyBehavior, IProviderBehavior { + /// <summary> + /// Initializes static members of the <see cref="AXFetchAsSregTransform"/> class. + /// </summary> + static AXFetchAsSregTransform() { + AXFormats = AXAttributeFormats.Common; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AXFetchAsSregTransform"/> class. + /// </summary> + public AXFetchAsSregTransform() { + } + + /// <summary> + /// Gets or sets the AX attribute type URI formats this transform is willing to work with. + /// </summary> + public static AXAttributeFormats AXFormats { get; set; } + + #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 + + #region IProviderBehavior 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 IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) { + // Nothing to do here. + } + + /// <summary> + /// Called when a request is received by the Provider. + /// </summary> + /// <param name="request">The incoming request.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + /// <remarks> + /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but + /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/> + /// itself as that instance may be shared across many requests. + /// </remarks> + bool IProviderBehavior.OnIncomingRequest(IRequest request) { + var extensionRequest = request as Provider.HostProcessedRequest; + if (extensionRequest != null) { + extensionRequest.UnifyExtensionsAsSreg(); + } + + return false; + } + + /// <summary> + /// Called when the Provider is preparing to send a response to an authentication request. + /// </summary> + /// <param name="request">The request that is configured to generate the outgoing response.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + bool IProviderBehavior.OnOutgoingResponse(Provider.IAuthenticationRequest request) { + request.ConvertSregToMatchRequest(); + return false; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.Designer.cs b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.Designer.cs new file mode 100644 index 0000000..8c952ab --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.Designer.cs @@ -0,0 +1,126 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30104.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.OpenId.Behaviors { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class BehaviorStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal BehaviorStrings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.OpenId.Behaviors.BehaviorStrings", typeof(BehaviorStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to The PAPE request has an incomplete set of authentication policies.. + /// </summary> + internal static string PapeRequestMissingRequiredPolicies { + get { + return ResourceManager.GetString("PapeRequestMissingRequiredPolicies", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A PAPE response is missing or is missing required policies.. + /// </summary> + internal static string PapeResponseOrRequiredPoliciesMissing { + get { + return ResourceManager.GetString("PapeResponseOrRequiredPoliciesMissing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No personally identifiable information should be included in authentication responses when the PAPE authentication policy http://www.idmanagement.gov/schema/2009/05/icam/no-pii.pdf is present.. + /// </summary> + internal static string PiiIncludedWithNoPiiPolicy { + get { + return ResourceManager.GetString("PiiIncludedWithNoPiiPolicy", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No personally identifiable information should be requested when the http://www.idmanagement.gov/schema/2009/05/icam/no-pii.pdf PAPE policy is present.. + /// </summary> + internal static string PiiRequestedWithNoPiiPolicy { + get { + return ResourceManager.GetString("PiiRequestedWithNoPiiPolicy", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No PPID provider has been configured.. + /// </summary> + internal static string PpidProviderNotGiven { + get { + return ResourceManager.GetString("PpidProviderNotGiven", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Discovery on the Realm URL MUST be performed before sending a positive assertion.. + /// </summary> + internal static string RealmDiscoveryNotPerformed { + get { + return ResourceManager.GetString("RealmDiscoveryNotPerformed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The Realm in an authentication request must be an HTTPS URL.. + /// </summary> + internal static string RealmMustBeHttps { + get { + return ResourceManager.GetString("RealmMustBeHttps", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.resx b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.resx new file mode 100644 index 0000000..a8bf2d6 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.resx @@ -0,0 +1,141 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="PapeRequestMissingRequiredPolicies" xml:space="preserve"> + <value>The PAPE request has an incomplete set of authentication policies.</value> + </data> + <data name="PapeResponseOrRequiredPoliciesMissing" xml:space="preserve"> + <value>A PAPE response is missing or is missing required policies.</value> + </data> + <data name="PiiIncludedWithNoPiiPolicy" xml:space="preserve"> + <value>No personally identifiable information should be included in authentication responses when the PAPE authentication policy http://www.idmanagement.gov/schema/2009/05/icam/no-pii.pdf is present.</value> + </data> + <data name="PiiRequestedWithNoPiiPolicy" xml:space="preserve"> + <value>No personally identifiable information should be requested when the http://www.idmanagement.gov/schema/2009/05/icam/no-pii.pdf PAPE policy is present.</value> + </data> + <data name="PpidProviderNotGiven" xml:space="preserve"> + <value>No PPID provider has been configured.</value> + </data> + <data name="RealmDiscoveryNotPerformed" xml:space="preserve"> + <value>Discovery on the Realm URL MUST be performed before sending a positive assertion.</value> + </data> + <data name="RealmMustBeHttps" xml:space="preserve"> + <value>The Realm in an authentication request must be an HTTPS URL.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.sr.resx b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.sr.resx new file mode 100644 index 0000000..2b1b911 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/BehaviorStrings.sr.resx @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="PpidProviderNotGiven" xml:space="preserve"> + <value>Nijedan PPID provajder nije konfigurisan.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/GsaIcamProfile.cs b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/GsaIcamProfile.cs new file mode 100644 index 0000000..66ac276 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/GsaIcamProfile.cs @@ -0,0 +1,294 @@ +//----------------------------------------------------------------------- +// <copyright file="GsaIcamProfile.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.Provider; + 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 GsaIcamProfile : IRelyingPartyBehavior, IProviderBehavior { + /// <summary> + /// The maximum time a shared association can live. + /// </summary> + private static readonly TimeSpan MaximumAssociationLifetime = TimeSpan.FromSeconds(86400); + + /// <summary> + /// Backing field for the <see cref="DisableSslRequirement"/> static property. + /// </summary> + private static bool disableSslRequirement = DotNetOpenAuthSection.Configuration.Messaging.RelaxSslRequirements; + + /// <summary> + /// Initializes a new instance of the <see cref="GsaIcamProfile"/> class. + /// </summary> + public GsaIcamProfile() { + if (DisableSslRequirement) { + Logger.OpenId.Warn("GSA level 1 behavior has its RequireSsl requirement disabled."); + } + } + + /// <summary> + /// Gets or sets the provider for generating PPID identifiers. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppid", Justification = "Acronym")] + public static IDirectedIdentityIdentifierProvider PpidIdentifierProvider { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether PII is allowed to be requested or received via OpenID. + /// </summary> + /// <value>The default value is <c>false</c>.</value> + public static bool AllowPersonallyIdentifiableInformation { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to ignore the SSL requirement (for testing purposes only). + /// </summary> + public static bool DisableSslRequirement { // not an auto-property because it has a default value, and FxCop doesn't want us using static constructors. + get { return disableSslRequirement; } + set { disableSslRequirement = value; } + } + + #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 + + #region IProviderBehavior Members + + /// <summary> + /// Adapts the default security settings to the requirements of this behavior. + /// </summary> + /// <param name="securitySettings">The original security settings.</param> + void IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) { + if (securitySettings.MaximumHashBitLength < 256) { + securitySettings.MaximumHashBitLength = 256; + } + + SetMaximumAssociationLifetimeToNotExceed(Protocol.Default.Args.SignatureAlgorithm.HMAC_SHA256, MaximumAssociationLifetime, securitySettings); + SetMaximumAssociationLifetimeToNotExceed(Protocol.Default.Args.SignatureAlgorithm.HMAC_SHA1, MaximumAssociationLifetime, securitySettings); + } + + /// <summary> + /// Called when a request is received by the Provider. + /// </summary> + /// <param name="request">The incoming request.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + /// <remarks> + /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but + /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/> + /// itself as that instance may be shared across many requests. + /// </remarks> + bool IProviderBehavior.OnIncomingRequest(IRequest request) { + var hostProcessedRequest = request as IHostProcessedRequest; + if (hostProcessedRequest != null) { + // Only apply our special policies if the RP requested it. + var papeRequest = request.GetExtension<PolicyRequest>(); + if (papeRequest != null) { + if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) { + // Whenever we see this GSA policy requested, we MUST also see the PPID policy requested. + ErrorUtilities.VerifyProtocol(papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier), BehaviorStrings.PapeRequestMissingRequiredPolicies); + ErrorUtilities.VerifyProtocol(string.Equals(hostProcessedRequest.Realm.Scheme, Uri.UriSchemeHttps, StringComparison.Ordinal) || DisableSslRequirement, BehaviorStrings.RealmMustBeHttps); + + // Apply GSA-specific security to this individual request. + request.SecuritySettings.RequireSsl = !DisableSslRequirement; + return true; + } + } + } + + return false; + } + + /// <summary> + /// Called when the Provider is preparing to send a response to an authentication request. + /// </summary> + /// <param name="request">The request that is configured to generate the outgoing response.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + bool IProviderBehavior.OnOutgoingResponse(Provider.IAuthenticationRequest request) { + bool result = false; + + // Nothing to do for negative assertions. + if (!request.IsAuthenticated.Value) { + return result; + } + + var requestInternal = (Provider.AuthenticationRequest)request; + var responseMessage = (IProtocolMessageWithExtensions)requestInternal.Response; + + // Only apply our special policies if the RP requested it. + var papeRequest = request.GetExtension<PolicyRequest>(); + if (papeRequest != null) { + var papeResponse = responseMessage.Extensions.OfType<PolicyResponse>().SingleOrDefault(); + if (papeResponse == null) { + request.AddResponseExtension(papeResponse = new PolicyResponse()); + } + + if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) { + result = true; + if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.USGovernmentTrustLevel1)) { + papeResponse.ActualPolicies.Add(AuthenticationPolicies.USGovernmentTrustLevel1); + } + + // The spec requires that the OP perform discovery and if that fails, it must either sternly + // warn the user of a potential threat or just abort the authentication. + // We can't verify that the OP displayed anything to the user at this level, but we can + // at least verify that the OP performed the discovery on the realm and halt things if it didn't. + ErrorUtilities.VerifyHost(requestInternal.HasRealmDiscoveryBeenPerformed, BehaviorStrings.RealmDiscoveryNotPerformed); + } + + if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { + ErrorUtilities.VerifyProtocol(request.ClaimedIdentifier == request.LocalIdentifier, OpenIdStrings.DelegatingIdentifiersNotAllowed); + + // Mask the user's identity with a PPID. + ErrorUtilities.VerifyHost(PpidIdentifierProvider != null, BehaviorStrings.PpidProviderNotGiven); + Identifier ppidIdentifier = PpidIdentifierProvider.GetIdentifier(request.LocalIdentifier, request.Realm); + requestInternal.ResetClaimedAndLocalIdentifiers(ppidIdentifier); + + // Indicate that the RP is receiving a PPID claimed_id + if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { + papeResponse.ActualPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier); + } + } + + if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) { + ErrorUtilities.VerifyProtocol( + !responseMessage.Extensions.OfType<ClaimsResponse>().Any() && + !responseMessage.Extensions.OfType<FetchResponse>().Any(), + BehaviorStrings.PiiIncludedWithNoPiiPolicy); + + // If no PII is given in extensions, and the claimed_id is a PPID, then we can state we issue no PII. + if (papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { + if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.NoPersonallyIdentifiableInformation)) { + papeResponse.ActualPolicies.Add(AuthenticationPolicies.NoPersonallyIdentifiableInformation); + } + } + } + + Reporting.RecordEventOccurrence(this, "OP"); + } + + return result; + } + + #endregion + + /// <summary> + /// Ensures the maximum association lifetime does not exceed a given limit. + /// </summary> + /// <param name="associationType">Type of the association.</param> + /// <param name="maximumLifetime">The maximum lifetime.</param> + /// <param name="securitySettings">The security settings to adjust.</param> + private static void SetMaximumAssociationLifetimeToNotExceed(string associationType, TimeSpan maximumLifetime, ProviderSecuritySettings securitySettings) { + Contract.Requires(!String.IsNullOrEmpty(associationType)); + Contract.Requires(maximumLifetime.TotalSeconds > 0); + if (!securitySettings.AssociationLifetimes.ContainsKey(associationType) || + securitySettings.AssociationLifetimes[associationType] > maximumLifetime) { + securitySettings.AssociationLifetimes[associationType] = maximumLifetime; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/PpidGeneration.cs b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/PpidGeneration.cs new file mode 100644 index 0000000..a465611 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Behaviors/PpidGeneration.cs @@ -0,0 +1,121 @@ +//----------------------------------------------------------------------- +// <copyright file="PpidGeneration.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.Messaging; + using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// Offers OpenID Providers automatic PPID Claimed Identifier generation when requested + /// by a PAPE request. + /// </summary> + /// <remarks> + /// <para>PPIDs are set on positive authentication responses when the PAPE request includes + /// the <see cref="AuthenticationPolicies.PrivatePersonalIdentifier"/> authentication policy.</para> + /// <para>The static member <see cref="PpidGeneration.PpidIdentifierProvider"/> MUST + /// be set prior to any PPID requests come in. Typically this should be set in the + /// <c>Application_Start</c> method in the global.asax.cs file.</para> + /// </remarks> + [Serializable] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppid", Justification = "Abbreviation")] + public sealed class PpidGeneration : IProviderBehavior { + /// <summary> + /// Gets or sets the provider for generating PPID identifiers. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppid", Justification = "Abbreviation")] + public static IDirectedIdentityIdentifierProvider PpidIdentifierProvider { get; set; } + + #region IProviderBehavior 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 IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) { + // No special security to apply here. + } + + /// <summary> + /// Called when a request is received by the Provider. + /// </summary> + /// <param name="request">The incoming request.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + /// <remarks> + /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but + /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/> + /// itself as that instance may be shared across many requests. + /// </remarks> + bool IProviderBehavior.OnIncomingRequest(IRequest request) { + return false; + } + + /// <summary> + /// Called when the Provider is preparing to send a response to an authentication request. + /// </summary> + /// <param name="request">The request that is configured to generate the outgoing response.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + bool IProviderBehavior.OnOutgoingResponse(IAuthenticationRequest request) { + // Nothing to do for negative assertions. + if (!request.IsAuthenticated.Value) { + return false; + } + + var requestInternal = (Provider.AuthenticationRequest)request; + var responseMessage = (IProtocolMessageWithExtensions)requestInternal.Response; + + // Only apply our special policies if the RP requested it. + var papeRequest = request.GetExtension<PolicyRequest>(); + if (papeRequest != null) { + if (papeRequest.PreferredPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { + ErrorUtilities.VerifyProtocol(request.ClaimedIdentifier == request.LocalIdentifier, OpenIdStrings.DelegatingIdentifiersNotAllowed); + + if (PpidIdentifierProvider == null) { + Logger.OpenId.Error(BehaviorStrings.PpidProviderNotGiven); + return false; + } + + // Mask the user's identity with a PPID. + if (PpidIdentifierProvider.IsUserLocalIdentifier(request.LocalIdentifier)) { + Identifier ppidIdentifier = PpidIdentifierProvider.GetIdentifier(request.LocalIdentifier, request.Realm); + requestInternal.ResetClaimedAndLocalIdentifiers(ppidIdentifier); + } + + // Indicate that the RP is receiving a PPID claimed_id + var papeResponse = responseMessage.Extensions.OfType<PolicyResponse>().SingleOrDefault(); + if (papeResponse == null) { + request.AddResponseExtension(papeResponse = new PolicyResponse()); + } + + if (!papeResponse.ActualPolicies.Contains(AuthenticationPolicies.PrivatePersonalIdentifier)) { + papeResponse.ActualPolicies.Add(AuthenticationPolicies.PrivatePersonalIdentifier); + } + + Reporting.RecordEventOccurrence(this, string.Empty); + } + } + + return false; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs new file mode 100644 index 0000000..b730b1f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/BackwardCompatibilityBindingElement.cs @@ -0,0 +1,129 @@ +//----------------------------------------------------------------------- +// <copyright file="BackwardCompatibilityBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Provides a mechanism for Relying Parties to work with OpenID 1.0 Providers + /// without losing claimed_id and op_endpoint data, which OpenID 2.0 Providers + /// are required to send back with positive assertions. + /// </summary> + internal class BackwardCompatibilityBindingElement : IChannelBindingElement { + /// <summary> + /// The "dnoa.op_endpoint" callback parameter that stores the Provider Endpoint URL + /// to tack onto the return_to URI. + /// </summary> + private const string ProviderEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; + + /// <summary> + /// The "dnoa.claimed_id" callback parameter that stores the Claimed Identifier + /// to tack onto the return_to URI. + /// </summary> + private const string ClaimedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + 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) { + SignedResponseRequest request = message as SignedResponseRequest; + if (request != null && request.Version.Major < 2) { + request.AddReturnToArguments(ProviderEndpointParameterName, request.Recipient.AbsoluteUri); + + CheckIdRequest authRequest = request as CheckIdRequest; + if (authRequest != null) { + request.AddReturnToArguments(ClaimedIdentifierParameterName, authRequest.ClaimedIdentifier); + } + + return MessageProtections.None; + } + + 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 (response != null && response.Version.Major < 2) { + // GetReturnToArgument may return parameters that are not signed, + // but we must allow for that since in OpenID 1.x, a stateless RP has + // no way to preserve the provider endpoint and claimed identifier otherwise. + // We'll verify the positive assertion later in the + // RelyingParty.PositiveAuthenticationResponse constructor anyway. + // If this is a 1.0 OP signed response without these parameters then we didn't initiate + // the request ,and since 1.0 OPs are not supposed to be able to send unsolicited + // assertions it's an invalid case that we throw an exception for. + if (response.ProviderEndpoint == null) { + string op_endpoint = response.GetReturnToArgument(ProviderEndpointParameterName); + ErrorUtilities.VerifyProtocol(op_endpoint != null, MessagingStrings.RequiredParametersMissing, message.GetType().Name, ProviderEndpointParameterName); + response.ProviderEndpoint = new Uri(op_endpoint); + } + + PositiveAssertionResponse authResponse = response as PositiveAssertionResponse; + if (authResponse != null) { + if (authResponse.ClaimedIdentifier == null) { + string claimedId = response.GetReturnToArgument(ClaimedIdentifierParameterName); + ErrorUtilities.VerifyProtocol(claimedId != null, MessagingStrings.RequiredParametersMissing, message.GetType().Name, ClaimedIdentifierParameterName); + authResponse.ClaimedIdentifier = claimedId; + } + } + + return MessageProtections.None; + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs new file mode 100644 index 0000000..c516e8f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ExtensionsBindingElement.cs @@ -0,0 +1,252 @@ +//----------------------------------------------------------------------- +// <copyright file="ExtensionsBindingElement.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.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// The binding element that serializes/deserializes OpenID extensions to/from + /// their carrying OpenID messages. + /// </summary> + internal class ExtensionsBindingElement : IChannelBindingElement { + /// <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 ExtensionsBindingElement(IOpenIdExtensionFactory extensionFactory, SecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(extensionFactory != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + + this.ExtensionFactory = extensionFactory; + this.relyingPartySecuritySettings = securitySettings as RelyingPartySecuritySettings; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the extension factory. + /// </summary> + public IOpenIdExtensionFactory ExtensionFactory { get; private set; } + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + 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> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "It doesn't look too bad to me. :)")] + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var extendableMessage = message as IProtocolMessageWithExtensions; + if (extendableMessage != null) { + Protocol protocol = Protocol.Lookup(message.Version); + MessageDictionary baseMessageDictionary = this.Channel.MessageDescriptions.GetAccessor(message); + + // We have a helper class that will do all the heavy-lifting of organizing + // all the extensions, their aliases, and their parameters. + var extensionManager = ExtensionArgumentsManager.CreateOutgoingExtensions(protocol); + foreach (IExtensionMessage protocolExtension in extendableMessage.Extensions) { + var extension = protocolExtension as IOpenIdMessageExtension; + if (extension != null) { + Reporting.RecordFeatureUse(protocolExtension); + + // Give extensions that require custom serialization a chance to do their work. + var customSerializingExtension = extension as IMessageWithEvents; + if (customSerializingExtension != null) { + customSerializingExtension.OnSending(); + } + + // OpenID 2.0 Section 12 forbids two extensions with the same TypeURI in the same message. + ErrorUtilities.VerifyProtocol(!extensionManager.ContainsExtension(extension.TypeUri), OpenIdStrings.ExtensionAlreadyAddedWithSameTypeURI, extension.TypeUri); + + // Ensure that we're sending out a valid extension. + var extensionDescription = this.Channel.MessageDescriptions.Get(extension); + var extensionDictionary = extensionDescription.GetDictionary(extension).Serialize(); + extensionDescription.EnsureMessagePartsPassBasicValidation(extensionDictionary); + + // Add the extension to the outgoing message payload. + extensionManager.AddExtensionArguments(extension.TypeUri, extensionDictionary); + } else { + Logger.OpenId.WarnFormat("Unexpected extension type {0} did not implement {1}.", protocolExtension.GetType(), typeof(IOpenIdMessageExtension).Name); + } + } + + // We use a cheap trick (for now at least) to determine whether the 'openid.' prefix + // belongs on the parameters by just looking at what other parameters do. + // Technically, direct message responses from Provider to Relying Party are the only + // messages that leave off the 'openid.' prefix. + bool includeOpenIdPrefix = baseMessageDictionary.Keys.Any(key => key.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)); + + // Add the extension parameters to the base message for transmission. + baseMessageDictionary.AddExtraParameters(extensionManager.GetArgumentsToSend(includeOpenIdPrefix)); + return MessageProtections.None; + } + + 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 extendableMessage = message as IProtocolMessageWithExtensions; + if (extendableMessage != null) { + // First add the extensions that are signed by the Provider. + foreach (IOpenIdMessageExtension signedExtension in this.GetExtensions(extendableMessage, true, null)) { + Reporting.RecordFeatureUse(signedExtension); + signedExtension.IsSignedByRemoteParty = true; + extendableMessage.Extensions.Add(signedExtension); + } + + // Now search again, considering ALL extensions whether they are signed or not, + // skipping the signed ones and adding the new ones as unsigned extensions. + if (this.relyingPartySecuritySettings == null || !this.relyingPartySecuritySettings.IgnoreUnsignedExtensions) { + Func<string, bool> isNotSigned = typeUri => !extendableMessage.Extensions.Cast<IOpenIdMessageExtension>().Any(ext => ext.TypeUri == typeUri); + foreach (IOpenIdMessageExtension unsignedExtension in this.GetExtensions(extendableMessage, false, isNotSigned)) { + Reporting.RecordFeatureUse(unsignedExtension); + unsignedExtension.IsSignedByRemoteParty = false; + extendableMessage.Extensions.Add(unsignedExtension); + } + } + + return MessageProtections.None; + } + + return null; + } + + #endregion + + /// <summary> + /// Gets the extensions on a message. + /// </summary> + /// <param name="message">The carrier of the extensions.</param> + /// <param name="ignoreUnsigned">If set to <c>true</c> only signed extensions will be available.</param> + /// <param name="extensionFilter">A optional filter that takes an extension type URI and + /// returns a value indicating whether that extension should be deserialized and + /// returned in the sequence. May be null.</param> + /// <returns>A sequence of extensions in the message.</returns> + private IEnumerable<IOpenIdMessageExtension> GetExtensions(IProtocolMessageWithExtensions message, bool ignoreUnsigned, Func<string, bool> extensionFilter) { + bool isAtProvider = message is SignedResponseRequest; + + // We have a helper class that will do all the heavy-lifting of organizing + // all the extensions, their aliases, and their parameters. + var extensionManager = ExtensionArgumentsManager.CreateIncomingExtensions(this.GetExtensionsDictionary(message, ignoreUnsigned)); + foreach (string typeUri in extensionManager.GetExtensionTypeUris()) { + // Our caller may have already obtained a signed version of this extension, + // so skip it if they don't want this one. + if (extensionFilter != null && !extensionFilter(typeUri)) { + continue; + } + + var extensionData = extensionManager.GetExtensionArguments(typeUri); + + // Initialize this particular extension. + IOpenIdMessageExtension extension = this.ExtensionFactory.Create(typeUri, extensionData, message, isAtProvider); + if (extension != null) { + try { + // Make sure the extension fulfills spec requirements before deserializing it. + MessageDescription messageDescription = this.Channel.MessageDescriptions.Get(extension); + messageDescription.EnsureMessagePartsPassBasicValidation(extensionData); + + // Deserialize the extension. + MessageDictionary extensionDictionary = messageDescription.GetDictionary(extension); + foreach (var pair in extensionData) { + extensionDictionary[pair.Key] = pair.Value; + } + + // Give extensions that require custom serialization a chance to do their work. + var customSerializingExtension = extension as IMessageWithEvents; + if (customSerializingExtension != null) { + customSerializingExtension.OnReceiving(); + } + } catch (ProtocolException ex) { + Logger.OpenId.ErrorFormat(OpenIdStrings.BadExtension, extension.GetType(), ex); + extension = null; + } + + if (extension != null) { + yield return extension; + } + } else { + Logger.OpenId.DebugFormat("Extension with type URI '{0}' ignored because it is not a recognized extension.", typeUri); + } + } + } + + /// <summary> + /// Gets the dictionary of message parts that should be deserialized into extensions. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="ignoreUnsigned">If set to <c>true</c> only signed extensions will be available.</param> + /// <returns> + /// A dictionary of message parts, including only signed parts when appropriate. + /// </returns> + private IDictionary<string, string> GetExtensionsDictionary(IProtocolMessage message, bool ignoreUnsigned) { + Contract.Requires<InvalidOperationException>(this.Channel != null); + + IndirectSignedResponse signedResponse = message as IndirectSignedResponse; + if (signedResponse != null && ignoreUnsigned) { + return signedResponse.GetSignedMessageParts(this.Channel); + } else { + return this.Channel.MessageDescriptions.GetAccessor(message); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs new file mode 100644 index 0000000..762fc9a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/IOpenIdExtensionFactory.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdExtensionFactory.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// OpenID extension factory class for creating extensions based on received Type URIs. + /// </summary> + /// <remarks> + /// OpenID extension factories must be registered with the library. This can be + /// done by adding a factory to <see cref="OpenIdRelyingParty.ExtensionFactories"/> + /// or <see cref="OpenIdProvider.ExtensionFactories"/>, or by adding a snippet + /// such as the following to your web.config file: + /// <example> + /// <dotNetOpenAuth> + /// <openid> + /// <extensionFactories> + /// <add type="DotNetOpenAuth.ApplicationBlock.CustomExtensions.Acme, DotNetOpenAuth.ApplicationBlock" /> + /// </extensionFactories> + /// </openid> + /// </dotNetOpenAuth> + /// </example> + /// </remarks> + public interface IOpenIdExtensionFactory { + /// <summary> + /// Creates a new instance of some extension based on the received extension parameters. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + /// <remarks> + /// This factory method need only initialize properties in the instantiated extension object + /// that are not bound using <see cref="MessagePartAttribute"/>. + /// </remarks> + IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole); + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs new file mode 100644 index 0000000..533e818 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ITamperResistantOpenIdMessage.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// <copyright file="ITamperResistantOpenIdMessage.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.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An interface that OAuth messages implement to support signing. + /// </summary> + internal interface ITamperResistantOpenIdMessage : ITamperResistantProtocolMessage, IReplayProtectedProtocolMessage { + /// <summary> + /// Gets or sets the association handle used to sign the message. + /// </summary> + /// <value>The handle for the association that was used to sign this assertion. </value> + string AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more. + /// </summary> + /// <value>If the Relying Party sent an invalid association handle with the request, it SHOULD be included here.</value> + string InvalidateHandle { get; set; } + + /// <summary> + /// Gets or sets the signed parameter order. + /// </summary> + /// <value>Comma-separated list of signed fields.</value> + /// <example>"op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce"</example> + /// <remarks> + /// This entry consists of the fields without the "openid." prefix that the signature covers. + /// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle", + /// and if present in the response, "claimed_id" and "identity". + /// Additional keys MAY be signed as part of the message. See Generating Signatures. + /// </remarks> + string SignedParameterOrder { get; set; } // TODO: make sure we have a unit test to verify that an incoming message with fewer signed fields than required will be rejected. + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs new file mode 100644 index 0000000..46c2139 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/KeyValueFormEncoding.cs @@ -0,0 +1,169 @@ +//----------------------------------------------------------------------- +// <copyright file="KeyValueFormEncoding.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.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Indicates the level of strictness to require when decoding a + /// Key-Value Form encoded dictionary. + /// </summary> + public enum KeyValueFormConformanceLevel { + /// <summary> + /// Be as forgiving as possible to errors made while encoding. + /// </summary> + Loose, + + /// <summary> + /// Allow for certain errors in encoding attributable to ambiguities + /// in the OpenID 1.1 spec's description of the encoding. + /// </summary> + OpenId11, + + /// <summary> + /// The strictest mode. The decoder requires the encoded dictionary + /// to be in strict compliance with OpenID 2.0's description of + /// the encoding. + /// </summary> + OpenId20, + } + + /// <summary> + /// Performs conversion to and from the Key-Value Form Encoding defined by + /// OpenID Authentication 2.0 section 4.1.1. + /// http://openid.net/specs/openid-authentication-2_0.html#anchor4 + /// </summary> + /// <remarks> + /// This class is thread safe and immutable. + /// </remarks> + internal class KeyValueFormEncoding { + /// <summary> + /// Characters that must not appear in parameter names. + /// </summary> + private static readonly char[] IllegalKeyCharacters = { '\n', ':' }; + + /// <summary> + /// Characters that must not appaer in parameter values. + /// </summary> + private static readonly char[] IllegalValueCharacters = { '\n' }; + + /// <summary> + /// The newline character sequence to use. + /// </summary> + private const string NewLineCharacters = "\n"; + + /// <summary> + /// The character encoding to use. + /// </summary> + private static readonly Encoding textEncoding = new UTF8Encoding(false); + + /// <summary> + /// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class. + /// </summary> + public KeyValueFormEncoding() { + this.ConformanceLevel = KeyValueFormConformanceLevel.Loose; + } + + /// <summary> + /// Initializes a new instance of the <see cref="KeyValueFormEncoding"/> class. + /// </summary> + /// <param name="conformanceLevel">How strictly an incoming Key-Value Form message will be held to the spec.</param> + public KeyValueFormEncoding(KeyValueFormConformanceLevel conformanceLevel) { + this.ConformanceLevel = conformanceLevel; + } + + /// <summary> + /// Gets a value controlling how strictly an incoming Key-Value Form message will be held to the spec. + /// </summary> + public KeyValueFormConformanceLevel ConformanceLevel { get; private set; } + + /// <summary> + /// Encodes key/value pairs to Key-Value Form. + /// </summary> + /// <param name="keysAndValues"> + /// The dictionary of key/value pairs to convert to a byte stream. + /// </param> + /// <returns>The UTF8 byte array.</returns> + /// <remarks> + /// Enumerating a Dictionary<TKey, TValue> has undeterministic ordering. + /// If ordering of the key=value pairs is important, a deterministic enumerator must + /// be used. + /// </remarks> + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Not a problem for this type.")] + public static byte[] GetBytes(IEnumerable<KeyValuePair<string, string>> keysAndValues) { + Contract.Requires<ArgumentNullException>(keysAndValues != null); + + using (MemoryStream ms = new MemoryStream()) { + using (StreamWriter sw = new StreamWriter(ms, textEncoding)) { + sw.NewLine = NewLineCharacters; + foreach (var pair in keysAndValues) { + if (pair.Key.IndexOfAny(IllegalKeyCharacters) >= 0) { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Key)); + } + if (pair.Value.IndexOfAny(IllegalValueCharacters) >= 0) { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidCharacterInKeyValueFormInput, pair.Value)); + } + + sw.Write(pair.Key); + sw.Write(':'); + sw.Write(pair.Value); + sw.WriteLine(); + } + } + + return ms.ToArray(); + } + } + + /// <summary> + /// Decodes bytes in Key-Value Form to key/value pairs. + /// </summary> + /// <param name="data">The stream of Key-Value Form encoded bytes.</param> + /// <returns>The deserialized dictionary.</returns> + /// <exception cref="FormatException">Thrown when the data is not in the expected format.</exception> + public IDictionary<string, string> GetDictionary(Stream data) { + using (StreamReader reader = new StreamReader(data, textEncoding)) { + var dict = new Dictionary<string, string>(); + int line_num = 0; + string line; + while ((line = reader.ReadLine()) != null) { + line_num++; + if (this.ConformanceLevel == KeyValueFormConformanceLevel.Loose) { + line = line.Trim(); + if (line.Length == 0) { + continue; + } + } + string[] parts = line.Split(new[] { ':' }, 2); + ErrorUtilities.VerifyFormat(parts.Length == 2, OpenIdStrings.InvalidKeyValueFormCharacterMissing, ':', line_num, line); + if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) { + ErrorUtilities.VerifyFormat(!(char.IsWhiteSpace(parts[0], parts[0].Length - 1) || char.IsWhiteSpace(parts[1], 0)), OpenIdStrings.InvalidCharacterInKeyValueFormInput, ' ', line_num, line); + } + if (this.ConformanceLevel < KeyValueFormConformanceLevel.OpenId20) { + parts[0] = parts[0].Trim(); + parts[1] = parts[1].Trim(); + } + + // calling Add method will throw if a key is encountered twice, + // which we should do. + dict.Add(parts[0], parts[1]); + } + if (this.ConformanceLevel > KeyValueFormConformanceLevel.Loose) { + reader.BaseStream.Seek(-1, SeekOrigin.End); + ErrorUtilities.VerifyFormat(reader.BaseStream.ReadByte() == '\n', OpenIdStrings.InvalidKeyValueFormCharacterMissing, "\\n", line_num, line); + } + return dict; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs new file mode 100644 index 0000000..d9a0e50 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdChannel.cs @@ -0,0 +1,386 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdChannel.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.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A channel that knows how to send and receive OpenID messages. + /// </summary> + [ContractVerification(true)] + internal class OpenIdChannel : Channel { + /// <summary> + /// The HTTP Content-Type to use in Key-Value Form responses. + /// </summary> + /// <remarks> + /// OpenID 2.0 section 5.1.2 says this SHOULD be text/plain. But this value + /// does not prevent free hosters like GoDaddy from tacking on their ads + /// to the end of the direct response, corrupting the data. So we deviate + /// from the spec a bit here to improve the story for free Providers. + /// </remarks> + internal const string KeyValueFormContentType = "application/x-openid-kvf"; + + /// <summary> + /// The encoder that understands how to read and write Key-Value Form. + /// </summary> + private KeyValueFormEncoding keyValueForm = new KeyValueFormEncoding(); + + /// <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 OpenIdChannel(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 Provider. + /// </summary> + /// <param name="cryptoKeyStore">The OpenID Provider's association store or handle encoder.</param> + /// <param name="nonceStore">The nonce store to use.</param> + /// <param name="securitySettings">The security settings.</param> + internal OpenIdChannel(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings) + : this(cryptoKeyStore, nonceStore, new OpenIdMessageFactory(), securitySettings) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + 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 OpenIdChannel(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> + /// Initializes a new instance of the <see cref="OpenIdChannel"/> class + /// for use by a Provider. + /// </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.</param> + private OpenIdChannel(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, IMessageFactory messageTypeProvider, ProviderSecuritySettings securitySettings) : + this(messageTypeProvider, InitializeBindingElements(cryptoKeyStore, nonceStore, securitySettings)) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Requires<ArgumentNullException>(messageTypeProvider != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdChannel"/> class. + /// </summary> + /// <param name="messageTypeProvider">A class prepared to analyze incoming messages and indicate what concrete + /// message types can deserialize from it.</param> + /// <param name="bindingElements">The binding elements to use in sending and receiving messages.</param> + private OpenIdChannel(IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements) + : base(messageTypeProvider, bindingElements) { + Contract.Requires<ArgumentNullException>(messageTypeProvider != null); + + // Customize the binding element order, since we play some tricks for higher + // security and backward compatibility with older OpenID versions. + var outgoingBindingElements = new List<IChannelBindingElement>(bindingElements); + var incomingBindingElements = new List<IChannelBindingElement>(bindingElements); + incomingBindingElements.Reverse(); + + // Customize the order of the incoming elements by moving the return_to elements in front. + var backwardCompatibility = incomingBindingElements.OfType<BackwardCompatibilityBindingElement>().SingleOrDefault(); + var returnToSign = incomingBindingElements.OfType<ReturnToSignatureBindingElement>().SingleOrDefault(); + if (backwardCompatibility != null) { + incomingBindingElements.MoveTo(0, backwardCompatibility); + } + if (returnToSign != null) { + // Yes, this is intentionally, shifting the backward compatibility + // binding element to second position. + incomingBindingElements.MoveTo(0, returnToSign); + } + + this.CustomizeBindingElementOrder(outgoingBindingElements, incomingBindingElements); + + // Change out the standard web request handler to reflect the standard + // OpenID pattern that outgoing web requests are to unknown and untrusted + // servers on the Internet. + this.WebRequestHandler = new UntrustedWebRequestHandler(); + } + + /// <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 OpenIdChannel(null, null, new OpenIdMessageFactory(), new RelyingPartySecuritySettings(), true); + } + + /// <summary> + /// Verifies the integrity and applicability of an incoming message. + /// </summary> + /// <param name="message">The message just received.</param> + /// <exception cref="ProtocolException"> + /// Thrown when the message is somehow invalid, except for check_authentication messages. + /// This can be due to tampering, replay attack or expiration, among other things. + /// </exception> + protected override void ProcessIncomingMessage(IProtocolMessage message) { + var checkAuthRequest = message as CheckAuthenticationRequest; + if (checkAuthRequest != null) { + IndirectSignedResponse originalResponse = new IndirectSignedResponse(checkAuthRequest, this); + try { + base.ProcessIncomingMessage(originalResponse); + checkAuthRequest.IsValid = true; + } catch (ProtocolException) { + checkAuthRequest.IsValid = false; + } + } else { + base.ProcessIncomingMessage(message); + } + + // Convert an OpenID indirect error message, which we never expect + // between two good OpenID implementations, into an exception. + // We don't process DirectErrorResponse because associate negotiations + // commonly get a derivative of that message type and handle it. + var errorMessage = message as IndirectErrorResponse; + if (errorMessage != null) { + string exceptionMessage = string.Format( + CultureInfo.CurrentCulture, + OpenIdStrings.IndirectErrorFormattedMessage, + errorMessage.ErrorMessage, + errorMessage.Contact, + errorMessage.Reference); + throw new ProtocolException(exceptionMessage, message); + } + } + + /// <summary> + /// Prepares an HTTP request that carries a given message. + /// </summary> + /// <param name="request">The message to send.</param> + /// <returns> + /// The <see cref="HttpWebRequest"/> prepared to send the request. + /// </returns> + protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { + return this.InitializeRequestAsPost(request); + } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + try { + return this.keyValueForm.GetDictionary(response.ResponseStream); + } catch (FormatException ex) { + throw ErrorUtilities.Wrap(ex, ex.Message); + } + } + + /// <summary> + /// Called when receiving a direct response message, before deserialization begins. + /// </summary> + /// <param name="response">The HTTP direct response.</param> + /// <param name="message">The newly instantiated message, prior to deserialization.</param> + protected override void OnReceivingDirectResponse(IncomingWebResponse response, IDirectResponseProtocolMessage message) { + base.OnReceivingDirectResponse(response, message); + + // Verify that the expected HTTP status code was used for the message, + // per OpenID 2.0 section 5.1.2.2. + // Note: The v1.1 spec doesn't require 400 responses for some error messages + if (message.Version.Major >= 2) { + var httpDirectResponse = message as IHttpDirectResponse; + if (httpDirectResponse != null) { + ErrorUtilities.VerifyProtocol( + httpDirectResponse.HttpStatusCode == response.Status, + MessagingStrings.UnexpectedHttpStatusCode, + (int)httpDirectResponse.HttpStatusCode, + (int)response.Status); + } + } + } + + /// <summary> + /// Queues a message for sending in the response stream where the fields + /// are sent in the response stream in querystring style. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns> + /// The pending user agent redirect based message to be sent as an HttpResponse. + /// </returns> + /// <remarks> + /// This method implements spec V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + var messageAccessor = this.MessageDescriptions.GetAccessor(response); + var fields = messageAccessor.Serialize(); + byte[] keyValueEncoding = KeyValueFormEncoding.GetBytes(fields); + + OutgoingWebResponse preparedResponse = new OutgoingWebResponse(); + preparedResponse.Headers.Add(HttpResponseHeader.ContentType, KeyValueFormContentType); + preparedResponse.OriginalMessage = response; + preparedResponse.ResponseStream = new MemoryStream(keyValueEncoding); + + IHttpDirectResponse httpMessage = response as IHttpDirectResponse; + if (httpMessage != null) { + preparedResponse.Status = httpMessage.HttpStatusCode; + } + + return preparedResponse; + } + + /// <summary> + /// Gets the direct response of a direct HTTP request. + /// </summary> + /// <param name="webRequest">The web request.</param> + /// <returns>The response to the web request.</returns> + /// <exception cref="ProtocolException">Thrown on network or protocol errors.</exception> + protected override IncomingWebResponse GetDirectResponse(HttpWebRequest webRequest) { + IncomingWebResponse response = this.WebRequestHandler.GetResponse(webRequest, DirectWebRequestOptions.AcceptAllHttpResponses); + + // Filter the responses to the allowable set of HTTP status codes. + if (response.Status != HttpStatusCode.OK && response.Status != HttpStatusCode.BadRequest) { + if (Logger.Channel.IsErrorEnabled) { + using (var reader = new StreamReader(response.ResponseStream)) { + Logger.Channel.ErrorFormat( + "Unexpected HTTP status code {0} {1} received in direct response:{2}{3}", + (int)response.Status, + response.Status, + Environment.NewLine, + reader.ReadToEnd()); + } + } + + // Call dispose before throwing since we're not including the response in the + // exception we're throwing. + response.Dispose(); + + ErrorUtilities.ThrowProtocol(OpenIdStrings.UnexpectedHttpStatusCode, (int)response.Status, response.Status); + } + + return response; + } + + /// <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(); + } + + /// <summary> + /// Initializes the binding elements. + /// </summary> + /// <param name="cryptoKeyStore">The OpenID Provider's 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> + /// <returns> + /// An array of binding elements which may be used to construct the channel. + /// </returns> + private static IChannelBindingElement[] InitializeBindingElements(IProviderAssociationStore cryptoKeyStore, INonceStore nonceStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + Contract.Requires<ArgumentNullException>(nonceStore != null); + + SigningBindingElement signingElement; + signingElement = new SigningBindingElement(cryptoKeyStore, securitySettings); + + var extensionFactory = OpenIdExtensionFactoryAggregator.LoadFromConfiguration(); + + List<IChannelBindingElement> elements = new List<IChannelBindingElement>(8); + elements.Add(new ExtensionsBindingElement(extensionFactory, securitySettings)); + elements.Add(new StandardReplayProtectionBindingElement(nonceStore, true)); + elements.Add(new StandardExpirationBindingElement()); + elements.Add(signingElement); + + return elements.ToArray(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdMessageFactory.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdMessageFactory.cs new file mode 100644 index 0000000..1e5ea4c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OpenIdMessageFactory.cs @@ -0,0 +1,146 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdMessageFactory.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.OpenId.Messages; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Distinguishes the various OpenID message types for deserialization purposes. + /// </summary> + internal class OpenIdMessageFactory : IMessageFactory { + #region IMessageFactory Members + + /// <summary> + /// Analyzes an incoming request message payload to discover what kind of + /// message is embedded in it and returns the type, or null if no match is found. + /// </summary> + /// <param name="recipient">The intended or actual recipient of the request message.</param> + /// <param name="fields">The name/value pairs that make up the message payload.</param> + /// <returns> + /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can + /// deserialize to. Null if the request isn't recognized as a valid protocol message. + /// </returns> + public IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) { + RequestBase message = null; + + // Discern the OpenID version of the message. + Protocol protocol = Protocol.V11; + string ns; + if (fields.TryGetValue(Protocol.V20.openid.ns, out ns)) { + ErrorUtilities.VerifyProtocol(string.Equals(ns, Protocol.OpenId2Namespace, StringComparison.Ordinal), MessagingStrings.UnexpectedMessagePartValue, Protocol.V20.openid.ns, ns); + protocol = Protocol.V20; + } + + string mode; + if (fields.TryGetValue(protocol.openid.mode, out mode)) { + if (string.Equals(mode, protocol.Args.Mode.associate)) { + if (fields.ContainsKey(protocol.openid.dh_consumer_public)) { + message = new AssociateDiffieHellmanRequest(protocol.Version, recipient.Location); + } else { + message = new AssociateUnencryptedRequest(protocol.Version, recipient.Location); + } + } else if (string.Equals(mode, protocol.Args.Mode.checkid_setup) || + string.Equals(mode, protocol.Args.Mode.checkid_immediate)) { + AuthenticationRequestMode authMode = string.Equals(mode, protocol.Args.Mode.checkid_immediate) ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; + if (fields.ContainsKey(protocol.openid.identity)) { + message = new CheckIdRequest(protocol.Version, recipient.Location, authMode); + } else { + ErrorUtilities.VerifyProtocol(!fields.ContainsKey(protocol.openid.claimed_id), OpenIdStrings.IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent); + message = new SignedResponseRequest(protocol.Version, recipient.Location, authMode); + } + } else if (string.Equals(mode, protocol.Args.Mode.cancel) || + (string.Equals(mode, protocol.Args.Mode.setup_needed) && (protocol.Version.Major >= 2 || fields.ContainsKey(protocol.openid.user_setup_url)))) { + message = new NegativeAssertionResponse(protocol.Version, recipient.Location, mode); + } else if (string.Equals(mode, protocol.Args.Mode.id_res)) { + if (fields.ContainsKey(protocol.openid.identity)) { + message = new PositiveAssertionResponse(protocol.Version, recipient.Location); + } else { + ErrorUtilities.VerifyProtocol(!fields.ContainsKey(protocol.openid.claimed_id), OpenIdStrings.IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent); + message = new IndirectSignedResponse(protocol.Version, recipient.Location); + } + } else if (string.Equals(mode, protocol.Args.Mode.check_authentication)) { + message = new CheckAuthenticationRequest(protocol.Version, recipient.Location); + } else if (string.Equals(mode, protocol.Args.Mode.error)) { + message = new IndirectErrorResponse(protocol.Version, recipient.Location); + } else { + ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessagePartValue, protocol.openid.mode, mode); + } + } + + if (message != null) { + message.SetAsIncoming(); + } + + return message; + } + + /// <summary> + /// Analyzes an incoming request message payload to discover what kind of + /// message is embedded in it and returns the type, or null if no match is found. + /// </summary> + /// <param name="request">The message that was sent as a request that resulted in the response.</param> + /// <param name="fields">The name/value pairs that make up the message payload.</param> + /// <returns> + /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can + /// deserialize to. Null if the request isn't recognized as a valid protocol message. + /// </returns> + public IDirectResponseProtocolMessage GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields) { + DirectResponseBase message = null; + + // Discern the OpenID version of the message. + Protocol protocol = Protocol.V11; + string ns; + if (fields.TryGetValue(Protocol.V20.openidnp.ns, out ns)) { + ErrorUtilities.VerifyProtocol(string.Equals(ns, Protocol.OpenId2Namespace, StringComparison.Ordinal), MessagingStrings.UnexpectedMessagePartValue, Protocol.V20.openidnp.ns, ns); + protocol = Protocol.V20; + } + + // Handle error messages generally. + if (fields.ContainsKey(protocol.openidnp.error)) { + message = new DirectErrorResponse(protocol.Version, request); + } + + var associateRequest = request as AssociateRequest; + if (associateRequest != null) { + if (protocol.Version.Major >= 2 && fields.ContainsKey(protocol.openidnp.error_code)) { + // This is a special recognized error case that we create a special message for. + message = new AssociateUnsuccessfulResponse(protocol.Version, associateRequest); + } else if (message == null) { + var associateDiffieHellmanRequest = request as AssociateDiffieHellmanRequest; + var associateUnencryptedRequest = request as AssociateUnencryptedRequest; + + if (associateDiffieHellmanRequest != null) { + message = new AssociateDiffieHellmanResponse(protocol.Version, associateDiffieHellmanRequest); + } + + if (associateUnencryptedRequest != null) { + message = new AssociateUnencryptedResponse(protocol.Version, associateUnencryptedRequest); + } + } + } + + var checkAuthenticationRequest = request as CheckAuthenticationRequest; + if (checkAuthenticationRequest != null && message == null) { + message = new CheckAuthenticationResponse(protocol.Version, checkAuthenticationRequest); + } + + if (message != null) { + message.SetAsIncoming(); + } + + return message; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs new file mode 100644 index 0000000..75b01e1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/OriginalStringUriEncoder.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="OriginalStringUriEncoder.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.Messaging.Reflection; + + /// <summary> + /// A Uri encoder that serializes using <see cref="Uri.OriginalString"/> + /// rather than the standard <see cref="Uri.AbsoluteUri"/>. + /// </summary> + internal class OriginalStringUriEncoder : IMessagePartEncoder { + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + Uri uriValue = (Uri)value; + return uriValue != null ? uriValue.OriginalString : null; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + return value != null ? new Uri(value) : null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/RelyingPartySecurityOptions.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/RelyingPartySecurityOptions.cs new file mode 100644 index 0000000..d8fc103 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/ChannelElements/ReturnToNonceBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToNonceBindingElement.cs new file mode 100644 index 0000000..3649543 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs new file mode 100644 index 0000000..30358e0 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs @@ -0,0 +1,211 @@ +//----------------------------------------------------------------------- +// <copyright file="ReturnToSignatureBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.ChannelElements { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// This binding element signs a Relying Party's openid.return_to parameter + /// so that upon return, it can verify that it hasn't been tampered with. + /// </summary> + /// <remarks> + /// <para>Since Providers can send unsolicited assertions, not all openid.return_to + /// values will be signed. But those that are signed will be validated, and + /// any invalid or missing signatures will cause this library to not trust + /// the parameters in the return_to URL.</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 doesn't sign + /// anything except a particular message part.</para> + /// </remarks> + internal class ReturnToSignatureBindingElement : IChannelBindingElement { + /// <summary> + /// The name of the callback parameter we'll tack onto the return_to value + /// to store our signature on the return_to parameter. + /// </summary> + private const string ReturnToSignatureParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig"; + + /// <summary> + /// The name of the callback parameter we'll tack onto the return_to value + /// to store the handle of the association we use to sign the return_to parameter. + /// </summary> + private const string ReturnToSignatureHandleParameterName = OpenIdUtilities.CustomParameterPrefix + "return_to_sig_handle"; + + /// <summary> + /// The URI to use for private associations at this RP. + /// </summary> + private static readonly Uri SecretUri = new Uri("https://localhost/dnoa/secret"); + + /// <summary> + /// The key store used to generate the private signature on the return_to parameter. + /// </summary> + private ICryptoKeyStore cryptoKeyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="ReturnToSignatureBindingElement"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + internal ReturnToSignatureBindingElement(ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + + this.cryptoKeyStore = cryptoKeyStore; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + /// <remarks> + /// No message protection is reported because this binding element + /// does not protect the entire message -- only a part. + /// </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) { + SignedResponseRequest request = message as SignedResponseRequest; + if (request != null && request.ReturnTo != null && request.SignReturnTo) { + var cryptoKeyPair = this.cryptoKeyStore.GetCurrentKey(SecretUri.AbsoluteUri, DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime); + request.AddReturnToArguments(ReturnToSignatureHandleParameterName, cryptoKeyPair.Key); + string signature = Convert.ToBase64String(this.GetReturnToSignature(request.ReturnTo, cryptoKeyPair.Value)); + request.AddReturnToArguments(ReturnToSignatureParameterName, signature); + + // We return none because we are not signing the entire message (only a part). + return MessageProtections.None; + } + + 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 (response != null) { + // We can't use response.GetReturnToArgument(string) because that relies + // on us already having validated this signature. + NameValueCollection returnToParameters = HttpUtility.ParseQueryString(response.ReturnTo.Query); + + // Only check the return_to signature if one is present. + if (returnToParameters[ReturnToSignatureHandleParameterName] != null) { + // Set the safety flag showing whether the return_to url had a valid signature. + byte[] expectedBytes = this.GetReturnToSignature(response.ReturnTo); + string actual = returnToParameters[ReturnToSignatureParameterName]; + actual = OpenIdUtilities.FixDoublyUriDecodedBase64String(actual); + byte[] actualBytes = Convert.FromBase64String(actual); + response.ReturnToParametersSignatureValidated = MessagingUtilities.AreEquivalentConstantTime(actualBytes, expectedBytes); + if (!response.ReturnToParametersSignatureValidated) { + Logger.Bindings.WarnFormat("The return_to signature failed verification."); + } + + return MessageProtections.None; + } + } + + return null; + } + + #endregion + + /// <summary> + /// Gets the return to signature. + /// </summary> + /// <param name="returnTo">The return to.</param> + /// <param name="cryptoKey">The crypto key.</param> + /// <returns> + /// The generated signature. + /// </returns> + /// <remarks> + /// Only the parameters in the return_to URI are signed, rather than the base URI + /// itself, in order that OPs that might change the return_to's implicit port :80 part + /// or other minor changes do not invalidate the signature. + /// </remarks> + private byte[] GetReturnToSignature(Uri returnTo, CryptoKey cryptoKey = null) { + Contract.Requires<ArgumentNullException>(returnTo != null); + + // Assemble the dictionary to sign, taking care to remove the signature itself + // in order to accurately reproduce the original signature (which of course didn't include + // the signature). + // Also we need to sort the dictionary's keys so that we sign in the same order as we did + // the last time. + var returnToParameters = HttpUtility.ParseQueryString(returnTo.Query); + returnToParameters.Remove(ReturnToSignatureParameterName); + var sortedReturnToParameters = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + foreach (string key in returnToParameters) { + sortedReturnToParameters.Add(key, returnToParameters[key]); + } + + Logger.Bindings.DebugFormat("ReturnTo signed data: {0}{1}", Environment.NewLine, sortedReturnToParameters.ToStringDeferred()); + + // Sign the parameters. + byte[] bytesToSign = KeyValueFormEncoding.GetBytes(sortedReturnToParameters); + byte[] signature; + try { + if (cryptoKey == null) { + cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]); + } + + using (var signer = new HMACSHA256(cryptoKey.Key)) { + signature = signer.ComputeHash(bytesToSign); + } + } catch (ProtocolException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.MaximumAuthenticationTimeExpired); + } + + return signature; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs new file mode 100644 index 0000000..e301a3e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SigningBindingElement.cs @@ -0,0 +1,410 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElement.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; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Net.Security; + using System.Web; + using DotNetOpenAuth.Loggers; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Signs and verifies authentication assertions. + /// </summary> + internal class SigningBindingElement : IChannelBindingElement { + /// <summary> + /// The association store used by Relying Parties to look up the secrets needed for signing. + /// </summary> + private readonly IRelyingPartyAssociationStore rpAssociations; + + /// <summary> + /// The association store used by Providers to look up the secrets needed for signing. + /// </summary> + private readonly IProviderAssociationStore opAssociations; + + /// <summary> + /// The security settings at the Provider. + /// Only defined when this element is instantiated to service a Provider. + /// </summary> + private readonly ProviderSecuritySettings opSecuritySettings; + + /// <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 SigningBindingElement(IRelyingPartyAssociationStore associationStore) { + this.rpAssociations = associationStore; + } + + /// <summary> + /// Initializes a new instance of the SigningBindingElement class for use by a Provider. + /// </summary> + /// <param name="associationStore">The association store used to look up the secrets needed for signing.</param> + /// <param name="securitySettings">The security settings.</param> + internal SigningBindingElement(IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(associationStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + + this.opAssociations = associationStore; + this.opSecuritySettings = securitySettings; + } + + #region IChannelBindingElement Properties + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + /// <value><see cref="MessageProtections.TamperProtection"/></value> + public MessageProtections Protection { + get { return MessageProtections.TamperProtection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + #endregion + + /// <summary> + /// Gets a value indicating whether this binding element is on a Provider channel. + /// </summary> + private bool IsOnProvider { + get { return this.opSecuritySettings != null; } + } + + #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> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.Bindings.DebugFormat("Signing {0} message.", message.GetType().Name); + Association association = this.GetAssociation(signedMessage); + signedMessage.AssociationHandle = association.Handle; + signedMessage.SignedParameterOrder = this.GetSignedParameterOrder(signedMessage); + signedMessage.Signature = this.GetSignature(signedMessage, association); + return MessageProtections.TamperProtection; + } + + 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> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.Bindings.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature); + MessageProtections protectionsApplied = MessageProtections.TamperProtection; + + this.EnsureParametersRequiringSignatureAreSigned(signedMessage); + + Association association = this.GetSpecificAssociation(signedMessage); + if (association != null) { + string signature = this.GetSignature(signedMessage, association); + if (!MessagingUtilities.EqualsConstantTime(signedMessage.Signature, signature)) { + Logger.Bindings.Error("Signature verification failed."); + throw new InvalidSignatureException(message); + } + } else { + ErrorUtilities.VerifyInternal(this.Channel != null, "Cannot verify private association signature because we don't have a channel."); + + // If we're on the Provider, then the RP sent us a check_auth with a signature + // we don't have an association for. (It may have expired, or it may be a faulty RP). + if (this.IsOnProvider) { + throw new InvalidSignatureException(message); + } + + // 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; + } + + return null; + } + + #endregion + + /// <summary> + /// Determines whether the relying party sending an authentication request is + /// vulnerable to replay attacks. + /// </summary> + /// <param name="request">The request message from the Relying Party. Useful, but may be null for conservative estimate results.</param> + /// <param name="response">The response message to be signed.</param> + /// <returns> + /// <c>true</c> if the relying party is vulnerable; otherwise, <c>false</c>. + /// </returns> + private static bool IsRelyingPartyVulnerableToReplays(SignedResponseRequest request, IndirectSignedResponse response) { + Contract.Requires<ArgumentNullException>(response != null); + + // OpenID 2.0 includes replay protection as part of the protocol. + if (response.Version.Major >= 2) { + return false; + } + + // This library's RP may be on the remote end, and may be using 1.x merely because + // discovery on the Claimed Identifier suggested this was a 1.x OP. + // Since this library's RP has a built-in request_nonce parameter for replay + // protection, we'll allow for that. + var returnToArgs = HttpUtility.ParseQueryString(response.ReturnTo.Query); + if (!string.IsNullOrEmpty(returnToArgs[ReturnToNonceBindingElement.NonceParameter])) { + return false; + } + + // If the OP endpoint _AND_ RP return_to URL uses HTTPS then no one + // can steal and replay the positive assertion. + // We can only ascertain this if the request message was handed to us + // so we know what our own OP endpoint is. If we don't have a request + // message, then we'll default to assuming it's insecure. + if (request != null) { + if (request.Recipient.IsTransportSecure() && response.Recipient.IsTransportSecure()) { + return false; + } + } + + // Nothing left to protect against replays. RP is vulnerable. + return true; + } + + /// <summary> + /// Ensures that all message parameters that must be signed are in fact included + /// in the signature. + /// </summary> + /// <param name="signedMessage">The signed message.</param> + private void EnsureParametersRequiringSignatureAreSigned(ITamperResistantOpenIdMessage signedMessage) { + // Verify that the signed parameter order includes the mandated fields. + // We do this in such a way that derived classes that add mandated fields automatically + // get included in the list of checked parameters. + Protocol protocol = Protocol.Lookup(signedMessage.Version); + var partsRequiringProtection = from part in this.Channel.MessageDescriptions.Get(signedMessage).Mapping.Values + where part.RequiredProtection != ProtectionLevel.None + where part.IsRequired || part.IsNondefaultValueSet(signedMessage) + select part.Name; + ErrorUtilities.VerifyInternal(partsRequiringProtection.All(name => name.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)), "Signing only works when the parameters start with the 'openid.' prefix."); + string[] signedParts = signedMessage.SignedParameterOrder.Split(','); + var unsignedParts = from partName in partsRequiringProtection + where !signedParts.Contains(partName.Substring(protocol.openid.Prefix.Length)) + select partName; + ErrorUtilities.VerifyProtocol(!unsignedParts.Any(), OpenIdStrings.SignatureDoesNotIncludeMandatoryParts, string.Join(", ", unsignedParts.ToArray())); + } + + /// <summary> + /// Calculates the signature for a given message. + /// </summary> + /// <param name="signedMessage">The message to sign or verify.</param> + /// <param name="association">The association to use to sign the message.</param> + /// <returns>The calculated signature of the method.</returns> + private string GetSignature(ITamperResistantOpenIdMessage signedMessage, Association association) { + Contract.Requires<ArgumentNullException>(signedMessage != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(signedMessage.SignedParameterOrder)); + Contract.Requires<ArgumentNullException>(association != null); + + // Prepare the parts to sign, taking care to replace an openid.mode value + // of check_authentication with its original id_res so the signature matches. + MessageDictionary dictionary = this.Channel.MessageDescriptions.GetAccessor(signedMessage); + var parametersToSign = from name in signedMessage.SignedParameterOrder.Split(',') + let prefixedName = Protocol.V20.openid.Prefix + name + select new KeyValuePair<string, string>(name, dictionary[prefixedName]); + + byte[] dataToSign = KeyValueFormEncoding.GetBytes(parametersToSign); + string signature = Convert.ToBase64String(association.Sign(dataToSign)); + + if (Logger.Signatures.IsDebugEnabled) { + Logger.Signatures.DebugFormat( + "Signing these message parts: {0}{1}{0}Base64 representation of signed data: {2}{0}Signature: {3}", + Environment.NewLine, + parametersToSign.ToStringDeferred(), + Convert.ToBase64String(dataToSign), + signature); + } + + return signature; + } + + /// <summary> + /// Gets the value to use for the openid.signed parameter. + /// </summary> + /// <param name="signedMessage">The signable message.</param> + /// <returns> + /// A comma-delimited list of parameter names, omitting the 'openid.' prefix, that determines + /// the inclusion and order of message parts that will be signed. + /// </returns> + private string GetSignedParameterOrder(ITamperResistantOpenIdMessage signedMessage) { + Contract.Requires<InvalidOperationException>(this.Channel != null); + Contract.Requires<ArgumentNullException>(signedMessage != null); + + Protocol protocol = Protocol.Lookup(signedMessage.Version); + + MessageDescription description = this.Channel.MessageDescriptions.Get(signedMessage); + var signedParts = from part in description.Mapping.Values + where (part.RequiredProtection & System.Net.Security.ProtectionLevel.Sign) != 0 + && part.GetValue(signedMessage) != null + select part.Name; + string prefix = Protocol.V20.openid.Prefix; + ErrorUtilities.VerifyInternal(signedParts.All(name => name.StartsWith(prefix, StringComparison.Ordinal)), "All signed message parts must start with 'openid.'."); + + if (this.opSecuritySettings.SignOutgoingExtensions) { + // Tack on any ExtraData parameters that start with 'openid.'. + List<string> extraSignedParameters = new List<string>(signedMessage.ExtraData.Count); + foreach (string key in signedMessage.ExtraData.Keys) { + if (key.StartsWith(protocol.openid.Prefix, StringComparison.Ordinal)) { + extraSignedParameters.Add(key); + } else { + Logger.Signatures.DebugFormat("The extra parameter '{0}' will not be signed because it does not start with 'openid.'.", key); + } + } + signedParts = signedParts.Concat(extraSignedParameters); + } + + int skipLength = prefix.Length; + string signedFields = string.Join(",", signedParts.Select(name => name.Substring(skipLength)).ToArray()); + return signedFields; + } + + /// <summary> + /// Gets the association to use to sign or verify a message. + /// </summary> + /// <param name="signedMessage">The message to sign or verify.</param> + /// <returns>The association to use to sign or verify the message.</returns> + private Association GetAssociation(ITamperResistantOpenIdMessage signedMessage) { + Contract.Requires<ArgumentNullException>(signedMessage != null); + + if (this.IsOnProvider) { + // We're on a Provider to either sign (smart/dumb) or verify a dumb signature. + bool signing = string.IsNullOrEmpty(signedMessage.Signature); + + if (signing) { + // If the RP has no replay protection, coerce use of a private association + // instead of a shared one (if security settings indicate) + // to protect the authenticating user from replay attacks. + bool forcePrivateAssociation = this.opSecuritySettings.ProtectDownlevelReplayAttacks + && IsRelyingPartyVulnerableToReplays(null, (IndirectSignedResponse)signedMessage); + + if (forcePrivateAssociation) { + if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) { + Logger.Signatures.Info("An OpenID 1.x authentication request with a shared association handle will be responded to with a private association in order to provide OP-side replay protection."); + } + + return this.GetDumbAssociationForSigning(); + } else { + return this.GetSpecificAssociation(signedMessage) ?? this.GetDumbAssociationForSigning(); + } + } else { + return this.GetSpecificAssociation(signedMessage); + } + } else { + // 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; + } + } + } + + /// <summary> + /// Gets a specific association referenced in a given message's association handle. + /// </summary> + /// <param name="signedMessage">The signed message whose association handle should be used to lookup the association to return.</param> + /// <returns>The referenced association; or <c>null</c> if such an association cannot be found.</returns> + /// <remarks> + /// If the association handle set in the message does not match any valid association, + /// the association handle property is cleared, and the + /// <see cref="ITamperResistantOpenIdMessage.InvalidateHandle"/> property is set to the + /// handle that could not be found. + /// </remarks> + private Association GetSpecificAssociation(ITamperResistantOpenIdMessage signedMessage) { + Association association = null; + + if (!string.IsNullOrEmpty(signedMessage.AssociationHandle)) { + IndirectSignedResponse indirectSignedMessage = signedMessage as IndirectSignedResponse; + if (this.IsOnProvider) { + // Since we have an association handle, we're either signing with a smart association, + // or verifying a dumb one. + bool signing = string.IsNullOrEmpty(signedMessage.Signature); + bool isPrivateAssociation = !signing; + association = this.opAssociations.Deserialize(signedMessage, isPrivateAssociation, signedMessage.AssociationHandle); + if (association == null) { + // There was no valid association with the requested handle. + // Let's tell the RP to forget about that association. + signedMessage.InvalidateHandle = signedMessage.AssociationHandle; + signedMessage.AssociationHandle = null; + } + } else if (this.rpAssociations != null) { // if on a smart RP + Uri providerEndpoint = indirectSignedMessage.ProviderEndpoint; + association = this.rpAssociations.GetAssociation(providerEndpoint, signedMessage.AssociationHandle); + } + } + + return association; + } + + /// <summary> + /// Gets a private Provider association used for signing messages in "dumb" mode. + /// </summary> + /// <returns>An existing or newly created association.</returns> + private Association GetDumbAssociationForSigning() { + // If no assoc_handle was given or it was invalid, the only thing + // left to do is sign a message using a 'dumb' mode association. + Protocol protocol = Protocol.Default; + Association association = HmacShaAssociation.Create(protocol, protocol.Args.SignatureAlgorithm.HMAC_SHA256, AssociationRelyingPartyType.Dumb, this.opAssociations, this.opSecuritySettings); + return association; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs new file mode 100644 index 0000000..ad65a83 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/SkipSecurityBindingElement.cs @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------- +// <copyright file="SkipSecurityBindingElement.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; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Spoofs security checks on incoming OpenID messages. + /// </summary> + internal class SkipSecurityBindingElement : IChannelBindingElement { + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <value></value> + /// <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> + /// <value><see cref="MessageProtections.All"/></value> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public MessageProtections Protection { + get { return MessageProtections.All; } + } + + /// <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) { + Debug.Fail("SkipSecurityBindingElement.ProcessOutgoingMessage should never be called."); + 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 signedMessage = message as ITamperResistantOpenIdMessage; + if (signedMessage != null) { + Logger.Bindings.DebugFormat("Skipped security checks of incoming {0} message for preview purposes.", message.GetType().Name); + return this.Protection; + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DHKeyGeneration.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DHKeyGeneration.cs new file mode 100644 index 0000000..6eca6a0 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DHKeyGeneration.cs @@ -0,0 +1,56 @@ +// <auto-generated /> + +// +// DHKeyGeneration.cs: Defines the different key generation methods. +// +// Author: +// Pieter Philippaerts (Pieter@mentalis.org) +// +// (C) 2003 The Mentalis.org Team (http://www.mentalis.org/) +// +// Source Code License +// Copyright 2002-2007, The Mentalis.org Team +// All rights reserved. +// http://www.mentalis.org/ +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +// - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// - Neither the name of the Mentalis.org Team, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +namespace Org.Mentalis.Security.Cryptography { + /// <summary> + /// Defines the different Diffie-Hellman key generation methods. + /// </summary> + internal enum DHKeyGeneration { + /* + /// <summary> + /// [TODO] you first randomly select a prime Q of size 160 bits, then choose P randomly among numbers like + /// Q*R+1 with R random. Then you go along with finding a generator G which has order exactly Q. The private + /// key X is then a number modulo Q. + /// [FIPS 186-2-Change1 -- http://csrc.nist.gov/publications/fips/] + /// </summary> + // see RFC2631 [http://www.faqs.org/rfcs/rfc2631.html] + DSA,*/ + /// <summary> + /// Returns dynamically generated values for P and G. Unlike the Sophie Germain or DSA key generation methods, + /// this method does not ensure that the selected prime offers an adequate security level. + /// </summary> + Random, + /* + /// <summary> + /// Returns dynamically generated values for P and G. P is a Sophie Germain prime, which has some interesting + /// security features when used with Diffie Hellman. + /// </summary> + SophieGermain,*/ + /// <summary> + /// Returns values for P and G that are hard coded in this library. Contrary to what your intuition may tell you, + /// using these hard coded values is perfectly safe. + /// The values of the P and G parameters are taken from 'The OAKLEY Key Determination Protocol' [RFC2412]. + /// This is the prefered key generation method, because it is very fast and very safe. + /// Because this method uses fixed values for the P and G parameters, not all bit sizes are supported. + /// The current implementation supports bit sizes of 768, 1024 and 1536. + /// </summary> + Static + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DHParameters.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DHParameters.cs new file mode 100644 index 0000000..8105125 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DHParameters.cs @@ -0,0 +1,38 @@ +// <auto-generated /> + +// +// DHParameters.cs: Defines a structure that holds the parameters of the Diffie-Hellman algorithm +// +// Author: +// Pieter Philippaerts (Pieter@mentalis.org) +// +// (C) 2003 The Mentalis.org Team (http://www.mentalis.org/) +// +// Source Code License +// Copyright 2002-2007, The Mentalis.org Team +// All rights reserved. +// http://www.mentalis.org/ +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +// - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// - Neither the name of the Mentalis.org Team, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +namespace Org.Mentalis.Security.Cryptography { + /// <summary> + /// Represents the parameters of the Diffie-Hellman algorithm. + /// </summary> + internal struct DHParameters { + /// <summary> + /// Represents the public <b>P</b> parameter of the Diffie-Hellman algorithm. + /// </summary> + public byte[] P; + /// <summary> + /// Represents the public <b>G</b> parameter of the Diffie-Hellman algorithm. + /// </summary> + public byte[] G; + /// <summary> + /// Represents the private <b>X</b> parameter of the Diffie-Hellman algorithm. + /// </summary> + public byte[] X; + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DiffieHellman.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DiffieHellman.cs new file mode 100644 index 0000000..5019f70 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DiffieHellman.cs @@ -0,0 +1,101 @@ +// <auto-generated /> + +// +// DiffieHellman.cs: Defines a base class from which all Diffie-Hellman implementations inherit +// +// Author: +// Pieter Philippaerts (Pieter@mentalis.org) +// +// (C) 2003 The Mentalis.org Team (http://www.mentalis.org/) +// +// +// Source Code License +// Copyright 2002-2007, The Mentalis.org Team +// All rights reserved. +// http://www.mentalis.org/ +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +// - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// - Neither the name of the Mentalis.org Team, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Security.Cryptography; + +namespace Org.Mentalis.Security.Cryptography { + /// <summary> + /// Defines a base class from which all Diffie-Hellman implementations inherit. + /// </summary> + internal abstract class DiffieHellman : AsymmetricAlgorithm { + /// <summary> + /// Creates an instance of the default implementation of the <see cref="DiffieHellman"/> algorithm. + /// </summary> + /// <returns>A new instance of the default implementation of DiffieHellman.</returns> + public static new DiffieHellman Create () { + return Create ("Mono.Security.Cryptography.DiffieHellman"); + } + /// <summary> + /// Creates an instance of the specified implementation of <see cref="DiffieHellman"/>. + /// </summary> + /// <param name="algName">The name of the implementation of DiffieHellman to use.</param> + /// <returns>A new instance of the specified implementation of DiffieHellman.</returns> + public static new DiffieHellman Create (string algName) { + return (DiffieHellman) CryptoConfig.CreateFromName (algName); + } + + /// <summary> + /// Initializes a new <see cref="DiffieHellman"/> instance. + /// </summary> + public DiffieHellman() {} + + /// <summary> + /// When overridden in a derived class, creates the key exchange data. + /// </summary> + /// <returns>The key exchange data to be sent to the intended recipient.</returns> + public abstract byte[] CreateKeyExchange(); + /// <summary> + /// When overridden in a derived class, extracts secret information from the key exchange data. + /// </summary> + /// <param name="keyEx">The key exchange data within which the secret information is hidden.</param> + /// <returns>The secret information derived from the key exchange data.</returns> + public abstract byte[] DecryptKeyExchange(byte[] keyEx); + + /// <summary> + /// When overridden in a derived class, exports the <see cref="DHParameters"/>. + /// </summary> + /// <param name="includePrivate"><b>true</b> to include private parameters; otherwise, <b>false</b>.</param> + /// <returns>The parameters for Diffie-Hellman.</returns> + public abstract DHParameters ExportParameters (bool includePrivate); + /// <summary> + /// When overridden in a derived class, imports the specified <see cref="DHParameters"/>. + /// </summary> + /// <param name="parameters">The parameters for Diffie-Hellman.</param> + public abstract void ImportParameters (DHParameters parameters); + +#if UNUSED + private byte[] GetNamedParam(SecurityElement se, string param) { + SecurityElement sep = se.SearchForChildByTag(param); + if (sep == null) + return null; + return Convert.FromBase64String(sep.Text); + } +#endif + /// <summary> + /// Reconstructs a <see cref="DiffieHellman"/> object from an XML string. + /// </summary> + /// <param name="xmlString">The XML string to use to reconstruct the DiffieHellman object.</param> + /// <exception cref="CryptographicException">One of the values in the XML string is invalid.</exception> + public override void FromXmlString (string xmlString) { + if (xmlString == null) + throw new ArgumentNullException (); + throw new NotImplementedException(); + } + /// <summary> + /// Creates and returns an XML string representation of the current <see cref="DiffieHellman"/> object. + /// </summary> + /// <param name="includePrivateParameters"><b>true</b> to include private parameters; otherwise, <b>false</b>.</param> + /// <returns>An XML string encoding of the current DiffieHellman object.</returns> + public override string ToXmlString (bool includePrivateParameters) { + throw new NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DiffieHellmanManaged.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DiffieHellmanManaged.cs new file mode 100644 index 0000000..6ac28df --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/DiffieHellmanManaged.cs @@ -0,0 +1,267 @@ +// <auto-generated /> + +// +// DiffieHellmanManaged.cs: Implements the Diffie-Hellman key agreement algorithm +// +// Author: +// Pieter Philippaerts (Pieter@mentalis.org) +// +// (C) 2003 The Mentalis.org Team (http://www.mentalis.org/) +// +// References: +// - PKCS#3 [http://www.rsasecurity.com/rsalabs/pkcs/pkcs-3/] +// +// +// Source Code License +// Copyright 2002-2007, The Mentalis.org Team +// All rights reserved. +// http://www.mentalis.org/ +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +// - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +// - Neither the name of the Mentalis.org Team, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +using System; +using System.Security.Cryptography; +using Mono.Math; + +namespace Org.Mentalis.Security.Cryptography { + /// <summary> + /// Implements the Diffie-Hellman algorithm. + /// </summary> + internal sealed class DiffieHellmanManaged : DiffieHellman { + /// <summary> + /// Initializes a new <see cref="DiffieHellmanManaged"/> instance. + /// </summary> + /// <remarks>The default length of the shared secret is 1024 bits.</remarks> + public DiffieHellmanManaged() : this(1024, 160, DHKeyGeneration.Static) {} + /// <summary> + /// Initializes a new <see cref="DiffieHellmanManaged"/> instance. + /// </summary> + /// <param name="bitlen">The length, in bits, of the public P parameter.</param> + /// <param name="l">The length, in bits, of the secret value X. This parameter can be set to 0 to use the default size.</param> + /// <param name="keygen">One of the <see cref="DHKeyGeneration"/> values.</param> + /// <remarks>The larger the bit length, the more secure the algorithm is. The default is 1024 bits. The minimum bit length is 128 bits.<br/>The size of the private value will be one fourth of the bit length specified.</remarks> + /// <exception cref="ArgumentException">The specified bit length is invalid.</exception> + public DiffieHellmanManaged(int bitlen, int l, DHKeyGeneration keygen) { + if (bitlen < 256 || l < 0) + throw new ArgumentException(); + BigInteger p, g; + GenerateKey(bitlen, keygen, out p, out g); + Initialize(p, g, null, l, false); + } + /// <summary> + /// Initializes a new <see cref="DiffieHellmanManaged"/> instance. + /// </summary> + /// <param name="p">The P parameter of the Diffie-Hellman algorithm. This is a public parameter.</param> + /// <param name="g">The G parameter of the Diffie-Hellman algorithm. This is a public parameter.</param> + /// <param name="x">The X parameter of the Diffie-Hellman algorithm. This is a private parameter. If this parameters is a null reference (<b>Nothing</b> in Visual Basic), a secret value of the default size will be generated.</param> + /// <exception cref="ArgumentNullException"><paramref name="p"/> or <paramref name="g"/> is a null reference (<b>Nothing</b> in Visual Basic).</exception> + /// <exception cref="CryptographicException"><paramref name="p"/> or <paramref name="g"/> is invalid.</exception> + public DiffieHellmanManaged(byte[] p, byte[] g, byte[] x) { + if (p == null || g == null) + throw new ArgumentNullException(); + if (x == null) + Initialize(new BigInteger(p), new BigInteger(g), null, 0, true); + else + Initialize(new BigInteger(p), new BigInteger(g), new BigInteger(x), 0, true); + } + /// <summary> + /// Initializes a new <see cref="DiffieHellmanManaged"/> instance. + /// </summary> + /// <param name="p">The P parameter of the Diffie-Hellman algorithm.</param> + /// <param name="g">The G parameter of the Diffie-Hellman algorithm.</param> + /// <param name="l">The length, in bits, of the private value. If 0 is specified, the default value will be used.</param> + /// <exception cref="ArgumentNullException"><paramref name="p"/> or <paramref name="g"/> is a null reference (<b>Nothing</b> in Visual Basic).</exception> + /// <exception cref="ArgumentException"><paramref name="l"/> is invalid.</exception> + /// <exception cref="CryptographicException"><paramref name="p"/> or <paramref name="g"/> is invalid.</exception> + public DiffieHellmanManaged(byte[] p, byte[] g, int l) { + if (p == null || g == null) + throw new ArgumentNullException(); + if (l < 0) + throw new ArgumentException(); + Initialize(new BigInteger(p), new BigInteger(g), null, l, true); + } + + // initializes the private variables (throws CryptographicException) + private void Initialize(BigInteger p, BigInteger g, BigInteger x, int secretLen, bool checkInput) { + if (!p.isProbablePrime() || g <= 0 || g >= p || (x != null && (x <= 0 || x > p - 2))) + throw new CryptographicException(); + // default is to generate a number as large as the prime this + // is usually overkill, but it's the most secure thing we can + // do if the user doesn't specify a desired secret length ... + if (secretLen == 0) + secretLen = p.bitCount(); + m_P = p; + m_G = g; + if (x == null) { + BigInteger pm1 = m_P - 1; + for(m_X = BigInteger.genRandom(secretLen); m_X >= pm1 || m_X == 0; m_X = BigInteger.genRandom(secretLen)) {} + } else { + m_X = x; + } + } + /// <summary> + /// Creates the key exchange data. + /// </summary> + /// <returns>The key exchange data to be sent to the intended recipient.</returns> + public override byte[] CreateKeyExchange() { + BigInteger y = m_G.modPow(m_X, m_P); + byte[] ret = y.getBytes(); + y.Clear(); + return ret; + } + /// <summary> + /// Extracts secret information from the key exchange data. + /// </summary> + /// <param name="keyEx">The key exchange data within which the shared key is hidden.</param> + /// <returns>The shared key derived from the key exchange data.</returns> + public override byte[] DecryptKeyExchange(byte[] keyEx) { + BigInteger pvr = new BigInteger(keyEx); + BigInteger z = pvr.modPow(m_X, m_P); + byte[] ret = z.getBytes(); + z.Clear(); + return ret; + } + /// <summary> + /// Gets the name of the key exchange algorithm. + /// </summary> + /// <value>The name of the key exchange algorithm.</value> + public override string KeyExchangeAlgorithm { + get { + return "1.2.840.113549.1.3"; // PKCS#3 OID + } + } + /// <summary> + /// Gets the name of the signature algorithm. + /// </summary> + /// <value>The name of the signature algorithm.</value> + public override string SignatureAlgorithm { + get { + return null; + } + } + /// <summary> + /// Releases the unmanaged resources used by the SymmetricAlgorithm and optionally releases the managed resources. + /// </summary> + /// <param name="disposing"><b>true</b> to release both managed and unmanaged resources; <b>false</b> to release only unmanaged resources.</param> + protected override void Dispose(bool disposing) { + if (!m_Disposed) { + m_P.Clear(); + m_G.Clear(); + m_X.Clear(); + } + m_Disposed = true; + } + /// <summary> + /// Exports the <see cref="DHParameters"/>. + /// </summary> + /// <param name="includePrivateParameters"><b>true</b> to include private parameters; otherwise, <b>false</b>.</param> + /// <returns>The parameters for <see cref="DiffieHellman"/>.</returns> + public override DHParameters ExportParameters(bool includePrivateParameters) { + DHParameters ret = new DHParameters(); + ret.P = m_P.getBytes(); + ret.G = m_G.getBytes(); + if (includePrivateParameters) { + ret.X = m_X.getBytes(); + } + return ret; + } + /// <summary> + /// Imports the specified <see cref="DHParameters"/>. + /// </summary> + /// <param name="parameters">The parameters for <see cref="DiffieHellman"/>.</param> + /// <exception cref="CryptographicException"><see cref="DHParameters.P">parameters.P</see> or <see cref="DHParameters.G">parameters.G</see> is a null reference (<b>Nothing</b> in Visual Basic) -or- <see cref="DHParameters.P">parameters.P</see> is not a prime number.</exception> + public override void ImportParameters(DHParameters parameters) { + if (parameters.P == null) + throw new CryptographicException("Missing P value."); + if (parameters.G == null) + throw new CryptographicException("Missing G value."); + + BigInteger p = new BigInteger(parameters.P), g = new BigInteger(parameters.G), x = null; + if (parameters.X != null) { + x = new BigInteger(parameters.X); + } + Initialize(p, g, x, 0, true); + } + /// <summary> + /// Releases the unmanaged resources used by the SymmetricAlgorithm. + /// </summary> + ~DiffieHellmanManaged() { + Dispose(false); + } + + //TODO: implement DH key generation methods + private void GenerateKey(int bitlen, DHKeyGeneration keygen, out BigInteger p, out BigInteger g) { + if (keygen == DHKeyGeneration.Static) { + if (bitlen == 768) + p = new BigInteger(m_OAKLEY768); + else if (bitlen == 1024) + p = new BigInteger(m_OAKLEY1024); + else if (bitlen == 1536) + p = new BigInteger(m_OAKLEY1536); + else + throw new ArgumentException("Invalid bit size."); + g = new BigInteger(22); // all OAKLEY keys use 22 as generator + //} else if (keygen == DHKeyGeneration.SophieGermain) { + // throw new NotSupportedException(); //TODO + //} else if (keygen == DHKeyGeneration.DSA) { + // 1. Let j = (p - 1)/q. + // 2. Set h = any integer, where 1 < h < p - 1 + // 3. Set g = h^j mod p + // 4. If g = 1 go to step 2 + // BigInteger j = (p - 1) / q; + } else { // random + p = BigInteger.genPseudoPrime(bitlen); + g = new BigInteger(3); // always use 3 as a generator + } + } + + private BigInteger m_P; + private BigInteger m_G; + private BigInteger m_X; + private bool m_Disposed; + + private byte[] m_OAKLEY768 = new byte[] { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, + 0x21, 0x68, 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, + 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, + 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, + 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, 0x30, 0x2B, 0x0A, 0x6D, + 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, + 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, + 0xA6, 0x3A, 0x36, 0x20, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + private byte[] m_OAKLEY1024 = new byte[] { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, + 0x21, 0x68, 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, + 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, + 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, + 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, 0x30, 0x2B, 0x0A, 0x6D, + 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, + 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, + 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, + 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, + 0x7C, 0x4B, 0x1F, 0xE6, 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE6, 0x53, 0x81, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + private byte[] m_OAKLEY1536 = new byte[] { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC9, 0x0F, 0xDA, 0xA2, + 0x21, 0x68, 0xC2, 0x34, 0xC4, 0xC6, 0x62, 0x8B, 0x80, 0xDC, 0x1C, 0xD1, + 0x29, 0x02, 0x4E, 0x08, 0x8A, 0x67, 0xCC, 0x74, 0x02, 0x0B, 0xBE, 0xA6, + 0x3B, 0x13, 0x9B, 0x22, 0x51, 0x4A, 0x08, 0x79, 0x8E, 0x34, 0x04, 0xDD, + 0xEF, 0x95, 0x19, 0xB3, 0xCD, 0x3A, 0x43, 0x1B, 0x30, 0x2B, 0x0A, 0x6D, + 0xF2, 0x5F, 0x14, 0x37, 0x4F, 0xE1, 0x35, 0x6D, 0x6D, 0x51, 0xC2, 0x45, + 0xE4, 0x85, 0xB5, 0x76, 0x62, 0x5E, 0x7E, 0xC6, 0xF4, 0x4C, 0x42, 0xE9, + 0xA6, 0x37, 0xED, 0x6B, 0x0B, 0xFF, 0x5C, 0xB6, 0xF4, 0x06, 0xB7, 0xED, + 0xEE, 0x38, 0x6B, 0xFB, 0x5A, 0x89, 0x9F, 0xA5, 0xAE, 0x9F, 0x24, 0x11, + 0x7C, 0x4B, 0x1F, 0xE6, 0x49, 0x28, 0x66, 0x51, 0xEC, 0xE4, 0x5B, 0x3D, + 0xC2, 0x00, 0x7C, 0xB8, 0xA1, 0x63, 0xBF, 0x05, 0x98, 0xDA, 0x48, 0x36, + 0x1C, 0x55, 0xD3, 0x9A, 0x69, 0x16, 0x3F, 0xA8, 0xFD, 0x24, 0xCF, 0x5F, + 0x83, 0x65, 0x5D, 0x23, 0xDC, 0xA3, 0xAD, 0x96, 0x1C, 0x62, 0xF3, 0x56, + 0x20, 0x85, 0x52, 0xBB, 0x9E, 0xD5, 0x29, 0x07, 0x70, 0x96, 0x96, 0x6D, + 0x67, 0x0C, 0x35, 0x4E, 0x4A, 0xBC, 0x98, 0x04, 0xF1, 0x74, 0x6C, 0x08, + 0xCA, 0x23, 0x73, 0x27, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF + }; + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/BigInteger.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/BigInteger.cs new file mode 100644 index 0000000..33df8df --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/BigInteger.cs @@ -0,0 +1,2241 @@ +// <auto-generated /> + +// +// BigInteger.cs - Big Integer implementation +// +// Authors: +// Ben Maurer +// Chew Keong TAN +// Sebastien Pouliot (spouliot@motus.com) +// +// Copyright (c) 2003 Ben Maurer +// All rights reserved +// +// Copyright (c) 2002 Chew Keong TAN +// All rights reserved. +// +// Modified 2007 Andrew Arnott (http://blog.nerdbank.net) +// Rewrote unsafe code as safe code. + +using System; +using System.Security.Cryptography; +using Mono.Math.Prime; +using Mono.Math.Prime.Generator; + +namespace Mono.Math { + + internal class BigInteger { + + #region Data Storage + + /// <summary> + /// The Length of this BigInteger + /// </summary> + uint length = 1; + + /// <summary> + /// The data for this BigInteger + /// </summary> + uint [] data; + + #endregion + + #region Constants + + /// <summary> + /// Default length of a BigInteger in bytes + /// </summary> + const uint DEFAULT_LEN = 20; + + /// <summary> + /// Table of primes below 2000. + /// </summary> + /// <remarks> + /// <para> + /// This table was generated using Mathematica 4.1 using the following function: + /// </para> + /// <para> + /// <code> + /// PrimeTable [x_] := Prime [Range [1, PrimePi [x]]] + /// PrimeTable [6000] + /// </code> + /// </para> + /// </remarks> + public static readonly uint [] smallPrimes = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, + 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, + 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, + 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, + 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, + 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, + 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, + 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, + 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, + 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, + 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997, + + 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069, 1087, + 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163, 1171, 1181, + 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279, + 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, + 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, + 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, + 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, + 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733, 1741, 1747, + 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, 1847, 1861, 1867, + 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, + 1979, 1987, 1993, 1997, 1999, + + 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069, 2081, 2083, 2087, 2089, + 2099, 2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, + 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297, + 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, + 2393, 2399, 2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, + 2521, 2531, 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, + 2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687, 2689, 2693, 2699, 2707, + 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753, 2767, 2777, 2789, 2791, 2797, + 2801, 2803, 2819, 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903, + 2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999, + + 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079, 3083, 3089, 3109, + 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191, 3203, 3209, 3217, 3221, + 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, + 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413, 3433, 3449, + 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517, 3527, 3529, 3533, 3539, + 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, + 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727, 3733, + 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821, 3823, 3833, 3847, 3851, + 3853, 3863, 3877, 3881, 3889, 3907, 3911, 3917, 3919, 3923, 3929, 3931, 3943, + 3947, 3967, 3989, + + 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057, 4073, 4079, 4091, + 4093, 4099, 4111, 4127, 4129, 4133, 4139, 4153, 4157, 4159, 4177, 4201, 4211, + 4217, 4219, 4229, 4231, 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, + 4297, 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409, 4421, 4423, + 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493, 4507, 4513, 4517, 4519, 4523, + 4547, 4549, 4561, 4567, 4583, 4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, + 4651, 4657, 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751, 4759, + 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831, 4861, 4871, 4877, 4889, + 4903, 4909, 4919, 4931, 4933, 4937, 4943, 4951, 4957, 4967, 4969, 4973, 4987, + 4993, 4999, + + 5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101, + 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231, + 5233, 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, + 5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449, + 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563, + 5569, 5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, 5657, 5659, 5669, + 5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791, + 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869, + 5879, 5881, 5897, 5903, 5923, 5927, 5939, 5953, 5981, 5987 + }; + + public enum Sign : int { + Negative = -1, + Zero = 0, + Positive = 1 + }; + + #region Exception Messages + const string WouldReturnNegVal = "Operation would return a negative value"; + #endregion + + #endregion + + #region Constructors + + public BigInteger () + { + data = new uint [DEFAULT_LEN]; + } + public BigInteger (Sign sign, uint len) + { + this.data = new uint [len]; + this.length = len; + } + + public BigInteger (BigInteger bi) + { + this.data = (uint [])bi.data.Clone (); + this.length = bi.length; + } + + public BigInteger (BigInteger bi, uint len) + { + + this.data = new uint [len]; + + for (uint i = 0; i < bi.length; i++) + this.data [i] = bi.data [i]; + + this.length = bi.length; + } + + #endregion + + public static BigInteger Parse(string number) { + if (number == null) + throw new ArgumentNullException(number); + int i = 0, len = number.Length; + char c; + bool digits_seen = false; + BigInteger val = new BigInteger(0); + if (number[i] == '+') { + i++; + } else if(number[i] == '-') { + throw new FormatException("Only positive integers are allowed."); + } + for(; i < len; i++) { + c = number[i]; + if (c == '\0') { + i = len; + continue; + } + if (c >= '0' && c <= '9'){ + val = val * 10 + (c - '0'); + digits_seen = true; + } else { + if (Char.IsWhiteSpace(c)){ + for (i++; i < len; i++){ + if (!Char.IsWhiteSpace (number[i])) + throw new FormatException(); + } + break; + } else + throw new FormatException(); + } + } + if (!digits_seen) + throw new FormatException(); + return val; + } + + #region Conversions + + public BigInteger (byte [] inData) + { + length = (uint)inData.Length >> 2; + int leftOver = inData.Length & 0x3; + + // length not multiples of 4 + if (leftOver != 0) length++; + + data = new uint [length]; + + for (int i = inData.Length - 1, j = 0; i >= 3; i -= 4, j++) { + data [j] = (uint)( + (inData [i-3] << (3*8)) | + (inData [i-2] << (2*8)) | + (inData [i-1] << (1*8)) | + (inData [i-0] << (0*8)) + ); + } + + switch (leftOver) { + case 1: data [length-1] = (uint)inData [0]; break; + case 2: data [length-1] = (uint)((inData [0] << 8) | inData [1]); break; + case 3: data [length-1] = (uint)((inData [0] << 16) | (inData [1] << 8) | inData [2]); break; + } + + this.Normalize (); + } + + public BigInteger (uint [] inData) + { + length = (uint)inData.Length; + + data = new uint [length]; + + for (int i = (int)length - 1, j = 0; i >= 0; i--, j++) + data [j] = inData [i]; + + this.Normalize (); + } + + public BigInteger (uint ui) + { + data = new uint [] {ui}; + } + + public BigInteger (ulong ul) + { + data = new uint [2] { (uint)ul, (uint)(ul >> 32)}; + length = 2; + + this.Normalize (); + } + + public static implicit operator BigInteger (uint value) + { + return (new BigInteger (value)); + } + + public static implicit operator BigInteger (int value) + { + if (value < 0) throw new ArgumentOutOfRangeException ("value"); + return (new BigInteger ((uint)value)); + } + + public static implicit operator BigInteger (ulong value) + { + return (new BigInteger (value)); + } + + #endregion + + #region Operators + + public static BigInteger operator + (BigInteger bi1, BigInteger bi2) + { + if (bi1 == 0) + return new BigInteger (bi2); + else if (bi2 == 0) + return new BigInteger (bi1); + else + return Kernel.AddSameSign (bi1, bi2); + } + + public static BigInteger operator - (BigInteger bi1, BigInteger bi2) + { + if (bi2 == 0) + return new BigInteger (bi1); + + if (bi1 == 0) + throw new ArithmeticException (WouldReturnNegVal); + + switch (Kernel.Compare (bi1, bi2)) { + + case Sign.Zero: + return 0; + + case Sign.Positive: + return Kernel.Subtract (bi1, bi2); + + case Sign.Negative: + throw new ArithmeticException (WouldReturnNegVal); + default: + throw new InvalidOperationException (); + } + } + + public static int operator % (BigInteger bi, int i) + { + if (i > 0) + return (int)Kernel.DwordMod (bi, (uint)i); + else + return -(int)Kernel.DwordMod (bi, (uint)-i); + } + + public static uint operator % (BigInteger bi, uint ui) + { + return Kernel.DwordMod (bi, (uint)ui); + } + + public static BigInteger operator % (BigInteger bi1, BigInteger bi2) + { + return Kernel.multiByteDivide (bi1, bi2)[1]; + } + + public static BigInteger operator / (BigInteger bi, int i) + { + if (i > 0) + return Kernel.DwordDiv (bi, (uint)i); + + throw new ArithmeticException (WouldReturnNegVal); + } + + public static BigInteger operator / (BigInteger bi1, BigInteger bi2) + { + return Kernel.multiByteDivide (bi1, bi2)[0]; + } + + public static BigInteger operator * (BigInteger bi1, BigInteger bi2) + { + if (bi1 == 0 || bi2 == 0) return 0; + + // + // Validate pointers + // + if (bi1.data.Length < bi1.length) throw new IndexOutOfRangeException ("bi1 out of range"); + if (bi2.data.Length < bi2.length) throw new IndexOutOfRangeException ("bi2 out of range"); + + BigInteger ret = new BigInteger (Sign.Positive, bi1.length + bi2.length); + + Kernel.Multiply (bi1.data, 0, bi1.length, bi2.data, 0, bi2.length, ret.data, 0); + + ret.Normalize (); + return ret; + } + + public static BigInteger operator * (BigInteger bi, int i) + { + if (i < 0) throw new ArithmeticException (WouldReturnNegVal); + if (i == 0) return 0; + if (i == 1) return new BigInteger (bi); + + return Kernel.MultiplyByDword (bi, (uint)i); + } + + public static BigInteger operator << (BigInteger bi1, int shiftVal) + { + return Kernel.LeftShift (bi1, shiftVal); + } + + public static BigInteger operator >> (BigInteger bi1, int shiftVal) + { + return Kernel.RightShift (bi1, shiftVal); + } + + #endregion + + #region Random + private static RandomNumberGenerator rng; + private static RandomNumberGenerator Rng { + get { + if (rng == null) + rng = RandomNumberGenerator.Create (); + return rng; + } + } + + /// <summary> + /// Generates a new, random BigInteger of the specified length. + /// </summary> + /// <param name="bits">The number of bits for the new number.</param> + /// <param name="rng">A random number generator to use to obtain the bits.</param> + /// <returns>A random number of the specified length.</returns> + public static BigInteger genRandom (int bits, RandomNumberGenerator rng) + { + int dwords = bits >> 5; + int remBits = bits & 0x1F; + + if (remBits != 0) + dwords++; + + BigInteger ret = new BigInteger (Sign.Positive, (uint)dwords + 1); + byte [] random = new byte [dwords << 2]; + + rng.GetBytes (random); + Buffer.BlockCopy (random, 0, ret.data, 0, (int)dwords << 2); + + if (remBits != 0) { + uint mask = (uint)(0x01 << (remBits-1)); + ret.data [dwords-1] |= mask; + + mask = (uint)(0xFFFFFFFF >> (32 - remBits)); + ret.data [dwords-1] &= mask; + } + else + ret.data [dwords-1] |= 0x80000000; + + ret.Normalize (); + return ret; + } + + /// <summary> + /// Generates a new, random BigInteger of the specified length using the default RNG crypto service provider. + /// </summary> + /// <param name="bits">The number of bits for the new number.</param> + /// <returns>A random number of the specified length.</returns> + public static BigInteger genRandom (int bits) + { + return genRandom (bits, Rng); + } + + /// <summary> + /// Randomizes the bits in "this" from the specified RNG. + /// </summary> + /// <param name="rng">A RNG.</param> + public void randomize (RandomNumberGenerator rng) + { + int bits = this.bitCount (); + int dwords = bits >> 5; + int remBits = bits & 0x1F; + + if (remBits != 0) + dwords++; + + byte [] random = new byte [dwords << 2]; + + rng.GetBytes (random); + Buffer.BlockCopy (random, 0, data, 0, (int)dwords << 2); + + if (remBits != 0) { + uint mask = (uint)(0x01 << (remBits-1)); + data [dwords-1] |= mask; + + mask = (uint)(0xFFFFFFFF >> (32 - remBits)); + data [dwords-1] &= mask; + } + + else + data [dwords-1] |= 0x80000000; + + Normalize (); + } + + /// <summary> + /// Randomizes the bits in "this" from the default RNG. + /// </summary> + public void randomize () + { + randomize (Rng); + } + + #endregion + + #region Bitwise + + public int bitCount () + { + this.Normalize (); + + uint value = data [length - 1]; + uint mask = 0x80000000; + uint bits = 32; + + while (bits > 0 && (value & mask) == 0) { + bits--; + mask >>= 1; + } + bits += ((length - 1) << 5); + + return (int)bits; + } + + /// <summary> + /// Tests if the specified bit is 1. + /// </summary> + /// <param name="bitNum">The bit to test. The least significant bit is 0.</param> + /// <returns>True if bitNum is set to 1, else false.</returns> + public bool testBit (uint bitNum) + { + uint bytePos = bitNum >> 5; // divide by 32 + byte bitPos = (byte)(bitNum & 0x1F); // get the lowest 5 bits + + uint mask = (uint)1 << bitPos; + return ((this.data [bytePos] & mask) != 0); + } + + public bool testBit (int bitNum) + { + if (bitNum < 0) throw new ArgumentOutOfRangeException ("bitNum"); + + uint bytePos = (uint)bitNum >> 5; // divide by 32 + byte bitPos = (byte)(bitNum & 0x1F); // get the lowest 5 bits + + uint mask = (uint)1 << bitPos; + return ((this.data [bytePos] | mask) == this.data [bytePos]); + } + + public void setBit (uint bitNum) + { + setBit (bitNum, true); + } + public void clearBit (uint bitNum) + { + setBit (bitNum, false); + } + + public void setBit (uint bitNum, bool val) + { + uint bytePos = bitNum >> 5; // divide by 32 + + if (bytePos < this.length) { + uint mask = (uint)1 << (int)(bitNum & 0x1F); + if (val) + this.data [bytePos] |= mask; + else + this.data [bytePos] &= ~mask; + } + } + + public int LowestSetBit () + { + if (this == 0) return -1; + int i = 0; + while (!testBit (i)) i++; + return i; + } + + public byte [] getBytes () + { + if (this == 0) return new byte [1]; + + int numBits = bitCount (); + int numBytes = numBits >> 3; + if ((numBits & 0x7) != 0) + numBytes++; + + byte [] result = new byte [numBytes]; + + int numBytesInWord = numBytes & 0x3; + if (numBytesInWord == 0) numBytesInWord = 4; + + int pos = 0; + for (int i = (int)length - 1; i >= 0; i--) { + uint val = data [i]; + for (int j = numBytesInWord - 1; j >= 0; j--) { + result [pos+j] = (byte)(val & 0xFF); + val >>= 8; + } + pos += numBytesInWord; + numBytesInWord = 4; + } + return result; + } + + #endregion + + #region Compare + + public static bool operator == (BigInteger bi1, uint ui) + { + if (bi1.length != 1) bi1.Normalize (); + return bi1.length == 1 && bi1.data [0] == ui; + } + + public static bool operator != (BigInteger bi1, uint ui) + { + if (bi1.length != 1) bi1.Normalize (); + return !(bi1.length == 1 && bi1.data [0] == ui); + } + + public static bool operator == (BigInteger bi1, BigInteger bi2) + { + // we need to compare with null + if ((bi1 as object) == (bi2 as object)) + return true; + if (null == bi1 || null == bi2) + return false; + return Kernel.Compare (bi1, bi2) == 0; + } + + public static bool operator != (BigInteger bi1, BigInteger bi2) + { + // we need to compare with null + if ((bi1 as object) == (bi2 as object)) + return false; + if (null == bi1 || null == bi2) + return true; + return Kernel.Compare (bi1, bi2) != 0; + } + + public static bool operator > (BigInteger bi1, BigInteger bi2) + { + return Kernel.Compare (bi1, bi2) > 0; + } + + public static bool operator < (BigInteger bi1, BigInteger bi2) + { + return Kernel.Compare (bi1, bi2) < 0; + } + + public static bool operator >= (BigInteger bi1, BigInteger bi2) + { + return Kernel.Compare (bi1, bi2) >= 0; + } + + public static bool operator <= (BigInteger bi1, BigInteger bi2) + { + return Kernel.Compare (bi1, bi2) <= 0; + } + + public Sign Compare (BigInteger bi) + { + return Kernel.Compare (this, bi); + } + + #endregion + + #region Formatting + + public string ToString (uint radix) + { + return ToString (radix, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + } + + public string ToString (uint radix, string charSet) + { + if (charSet.Length < radix) + throw new ArgumentException ("charSet length less than radix", "charSet"); + if (radix == 1) + throw new ArgumentException ("There is no such thing as radix one notation", "radix"); + + if (this == 0) return "0"; + if (this == 1) return "1"; + + string result = ""; + + BigInteger a = new BigInteger (this); + + while (a != 0) { + uint rem = Kernel.SingleByteDivideInPlace (a, radix); + result = charSet [ (int)rem] + result; + } + + return result; + } + + #endregion + + #region Misc + + /// <summary> + /// Normalizes this by setting the length to the actual number of + /// uints used in data and by setting the sign to Sign.Zero if the + /// value of this is 0. + /// </summary> + private void Normalize () + { + // Normalize length + while (length > 0 && data [length-1] == 0) length--; + + // Check for zero + if (length == 0) + length++; + } + + public void Clear () + { + for (int i=0; i < length; i++) + data [i] = 0x00; + } + + #endregion + + #region Object Impl + + public override int GetHashCode () + { + uint val = 0; + + for (uint i = 0; i < this.length; i++) + val ^= this.data [i]; + + return (int)val; + } + + public override string ToString () + { + return ToString (10); + } + + public override bool Equals (object o) + { + if (o == null) return false; + if (o is int) return (int)o >= 0 && this == (uint)o; + + return Kernel.Compare (this, (BigInteger)o) == 0; + } + + #endregion + + #region Number Theory + + public BigInteger gcd (BigInteger bi) + { + return Kernel.gcd (this, bi); + } + + public BigInteger modInverse (BigInteger mod) + { + return Kernel.modInverse (this, mod); + } + + public BigInteger modPow (BigInteger exp, BigInteger n) + { + ModulusRing mr = new ModulusRing (n); + return mr.Pow (this, exp); + } + + #endregion + + #region Prime Testing + + public bool isProbablePrime () + { + + for (int p = 0; p < smallPrimes.Length; p++) { + if (this == smallPrimes [p]) + return true; + if (this % smallPrimes [p] == 0) + return false; + } + + return PrimalityTests.RabinMillerTest (this, Prime.ConfidenceFactor.Medium); + } + + [Obsolete] + public bool isProbablePrime (int notUsed) + { + + for (int p = 0; p < smallPrimes.Length; p++) { + if (this % smallPrimes [p] == 0) + return false; + } + + return + PrimalityTests.SmallPrimeSppTest (this, Prime.ConfidenceFactor.Medium); + } + + #endregion + + #region Prime Number Generation + + /// <summary> + /// Generates the smallest prime >= bi + /// </summary> + /// <param name="bi">A BigInteger</param> + /// <returns>The smallest prime >= bi. More mathematically, if bi is prime: bi, else Prime [PrimePi [bi] + 1].</returns> + public static BigInteger NextHightestPrime (BigInteger bi) + { + NextPrimeFinder npf = new NextPrimeFinder (); + return npf.GenerateNewPrime (0, bi); + } + + public static BigInteger genPseudoPrime (int bits) + { + SequentialSearchPrimeGeneratorBase sspg = new SequentialSearchPrimeGeneratorBase (); + return sspg.GenerateNewPrime (bits); + } + + /// <summary> + /// Increments this by two + /// </summary> + public void Incr2 () + { + int i = 0; + + data [0] += 2; + + // If there was no carry, nothing to do + if (data [0] < 2) { + + // Account for the first carry + data [++i]++; + + // Keep adding until no carry + while (data [i++] == 0x0) + data [i]++; + + // See if we increased the data length + if (length == (uint)i) + length++; + } + } + + #endregion + + public sealed class ModulusRing { + + BigInteger mod, constant; + + public ModulusRing (BigInteger mod) + { + this.mod = mod; + + // calculate constant = b^ (2k) / m + uint i = mod.length << 1; + + constant = new BigInteger (Sign.Positive, i + 1); + constant.data [i] = 0x00000001; + + constant = constant / mod; + } + + public void BarrettReduction (BigInteger x) + { + BigInteger n = mod; + uint k = n.length, + kPlusOne = k+1, + kMinusOne = k-1; + + // x < mod, so nothing to do. + if (x.length < k) return; + + BigInteger q3; + + // + // Validate pointers + // + if (x.data.Length < x.length) throw new IndexOutOfRangeException ("x out of range"); + + // q1 = x / b^ (k-1) + // q2 = q1 * constant + // q3 = q2 / b^ (k+1), Needs to be accessed with an offset of kPlusOne + + // TODO: We should the method in HAC p 604 to do this (14.45) + q3 = new BigInteger (Sign.Positive, x.length - kMinusOne + constant.length); + Kernel.Multiply (x.data, kMinusOne, x.length - kMinusOne, constant.data, 0, constant.length, q3.data, 0); + + // r1 = x mod b^ (k+1) + // i.e. keep the lowest (k+1) words + + uint lengthToCopy = (x.length > kPlusOne) ? kPlusOne : x.length; + + x.length = lengthToCopy; + x.Normalize (); + + // r2 = (q3 * n) mod b^ (k+1) + // partial multiplication of q3 and n + + BigInteger r2 = new BigInteger (Sign.Positive, kPlusOne); + Kernel.MultiplyMod2p32pmod (q3.data, (int)kPlusOne, (int)q3.length - (int)kPlusOne, n.data, 0, (int)n.length, r2.data, 0, (int)kPlusOne); + + r2.Normalize (); + + if (r2 < x) { + Kernel.MinusEq (x, r2); + } else { + BigInteger val = new BigInteger (Sign.Positive, kPlusOne + 1); + val.data [kPlusOne] = 0x00000001; + + Kernel.MinusEq (val, r2); + Kernel.PlusEq (x, val); + } + + while (x >= n) + Kernel.MinusEq (x, n); + } + + public BigInteger Multiply (BigInteger a, BigInteger b) + { + if (a == 0 || b == 0) return 0; + + if (a.length >= mod.length << 1) + a %= mod; + + if (b.length >= mod.length << 1) + b %= mod; + + if (a.length >= mod.length) + BarrettReduction (a); + + if (b.length >= mod.length) + BarrettReduction (b); + + BigInteger ret = new BigInteger (a * b); + BarrettReduction (ret); + + return ret; + } + + public BigInteger Difference (BigInteger a, BigInteger b) + { + Sign cmp = Kernel.Compare (a, b); + BigInteger diff; + + switch (cmp) { + case Sign.Zero: + return 0; + case Sign.Positive: + diff = a - b; break; + case Sign.Negative: + diff = b - a; break; + default: + throw new InvalidOperationException(); + } + + if (diff >= mod) { + if (diff.length >= mod.length << 1) + diff %= mod; + else + BarrettReduction (diff); + } + if (cmp == Sign.Negative) + diff = mod - diff; + return diff; + } + + public BigInteger Pow (BigInteger b, BigInteger exp) + { + if ((mod.data [0] & 1) == 1) return OddPow (b, exp); + else return EvenPow (b, exp); + } + + public BigInteger EvenPow (BigInteger b, BigInteger exp) + { + BigInteger resultNum = new BigInteger ((BigInteger)1, mod.length << 1); + BigInteger tempNum = new BigInteger (b % mod, mod.length << 1); // ensures (tempNum * tempNum) < b^ (2k) + + uint totalBits = (uint)exp.bitCount (); + + uint [] wkspace = new uint [mod.length << 1]; + + // perform squaring and multiply exponentiation + for (uint pos = 0; pos < totalBits; pos++) { + if (exp.testBit (pos)) { + + Array.Clear (wkspace, 0, wkspace.Length); + Kernel.Multiply (resultNum.data, 0, resultNum.length, tempNum.data, 0, tempNum.length, wkspace, 0); + resultNum.length += tempNum.length; + uint [] t = wkspace; + wkspace = resultNum.data; + resultNum.data = t; + + BarrettReduction (resultNum); + } + + Kernel.SquarePositive (tempNum, ref wkspace); + BarrettReduction (tempNum); + + if (tempNum == 1) { + return resultNum; + } + } + + return resultNum; + } + + private BigInteger OddPow (BigInteger b, BigInteger exp) + { + BigInteger resultNum = new BigInteger (Montgomery.ToMont (1, mod), mod.length << 1); + BigInteger tempNum = new BigInteger (Montgomery.ToMont (b, mod), mod.length << 1); // ensures (tempNum * tempNum) < b^ (2k) + uint mPrime = Montgomery.Inverse (mod.data [0]); + uint totalBits = (uint)exp.bitCount (); + + uint [] wkspace = new uint [mod.length << 1]; + + // perform squaring and multiply exponentiation + for (uint pos = 0; pos < totalBits; pos++) { + if (exp.testBit (pos)) { + + Array.Clear (wkspace, 0, wkspace.Length); + Kernel.Multiply (resultNum.data, 0, resultNum.length, tempNum.data, 0, tempNum.length, wkspace, 0); + resultNum.length += tempNum.length; + uint [] t = wkspace; + wkspace = resultNum.data; + resultNum.data = t; + + Montgomery.Reduce (resultNum, mod, mPrime); + } + + Kernel.SquarePositive (tempNum, ref wkspace); + Montgomery.Reduce (tempNum, mod, mPrime); + } + + Montgomery.Reduce (resultNum, mod, mPrime); + return resultNum; + } + + #region Pow Small Base + + // TODO: Make tests for this, not really needed b/c prime stuff + // checks it, but still would be nice + public BigInteger Pow (uint b, BigInteger exp) + { + if (b != 2) { + if ((mod.data [0] & 1) == 1) return OddPow (b, exp); + else return EvenPow (b, exp); + } else { + if ((mod.data [0] & 1) == 1) return OddModTwoPow (exp); + else return EvenModTwoPow (exp); + } + } + + private BigInteger OddPow (uint b, BigInteger exp) + { + exp.Normalize (); + uint [] wkspace = new uint [mod.length << 1 + 1]; + + BigInteger resultNum = Montgomery.ToMont ((BigInteger)b, this.mod); + resultNum = new BigInteger (resultNum, mod.length << 1 +1); + + uint mPrime = Montgomery.Inverse (mod.data [0]); + + uint pos = (uint)exp.bitCount () - 2; + + // + // We know that the first itr will make the val b + // + + do { + // + // r = r ^ 2 % m + // + Kernel.SquarePositive(resultNum, ref wkspace); + resultNum = Montgomery.Reduce(resultNum, mod, mPrime); + + if (exp.testBit(pos)) { + + // + // r = r * b % m + // + + uint u = 0; + + uint i = 0; + ulong mc = 0; + + do { + mc += (ulong)resultNum.data[u + i] * (ulong)b; + resultNum.data[u + i] = (uint)mc; + mc >>= 32; + } while (++i < resultNum.length); + + if (resultNum.length < mod.length) { + if (mc != 0) { + resultNum.data[u + i] = (uint)mc; + resultNum.length++; + while (resultNum >= mod) + Kernel.MinusEq(resultNum, mod); + } + } else if (mc != 0) { + + // + // First, we estimate the quotient by dividing + // the first part of each of the numbers. Then + // we correct this, if necessary, with a subtraction. + // + + uint cc = (uint)mc; + + // We would rather have this estimate overshoot, + // so we add one to the divisor + uint divEstimate = (uint)((((ulong)cc << 32) | (ulong)resultNum.data[u + i - 1]) / + (mod.data[mod.length - 1] + 1)); + + uint t; + + i = 0; + mc = 0; + do { + mc += (ulong)mod.data[i] * (ulong)divEstimate; + t = resultNum.data[u + i]; + resultNum.data[u + i] -= (uint)mc; + mc >>= 32; + if (resultNum.data[u + i] > t) mc++; + i++; + } while (i < resultNum.length); + cc -= (uint)mc; + + if (cc != 0) { + + uint sc = 0, j = 0; + uint[] s = mod.data; + do { + uint a = s[j]; + if (((a += sc) < sc) | ((resultNum.data[u + j] -= a) > ~a)) sc = 1; + else sc = 0; + j++; + } while (j < resultNum.length); + cc -= sc; + } + while (resultNum >= mod) + Kernel.MinusEq(resultNum, mod); + } else { + while (resultNum >= mod) + Kernel.MinusEq(resultNum, mod); + } + } + } while (pos-- > 0); + + resultNum = Montgomery.Reduce (resultNum, mod, mPrime); + return resultNum; + + } + + private BigInteger EvenPow(uint b, BigInteger exp) { + exp.Normalize(); + uint[] wkspace = new uint[mod.length << 1 + 1]; + BigInteger resultNum = new BigInteger((BigInteger)b, mod.length << 1 + 1); + + uint pos = (uint)exp.bitCount() - 2; + + // + // We know that the first itr will make the val b + // + + do { + // + // r = r ^ 2 % m + // + Kernel.SquarePositive(resultNum, ref wkspace); + if (!(resultNum.length < mod.length)) + BarrettReduction(resultNum); + + if (exp.testBit(pos)) { + + // + // r = r * b % m + // + + uint u = 0; + + uint i = 0; + ulong mc = 0; + + do { + mc += (ulong)resultNum.data[u + i] * (ulong)b; + resultNum.data[u + i] = (uint)mc; + mc >>= 32; + } while (++i < resultNum.length); + + if (resultNum.length < mod.length) { + if (mc != 0) { + resultNum.data[u + i] = (uint)mc; + resultNum.length++; + while (resultNum >= mod) + Kernel.MinusEq(resultNum, mod); + } + } else if (mc != 0) { + + // + // First, we estimate the quotient by dividing + // the first part of each of the numbers. Then + // we correct this, if necessary, with a subtraction. + // + + uint cc = (uint)mc; + + // We would rather have this estimate overshoot, + // so we add one to the divisor + uint divEstimate = (uint)((((ulong)cc << 32) | (ulong)resultNum.data[u + i - 1]) / + (mod.data[mod.length - 1] + 1)); + + uint t; + + i = 0; + mc = 0; + do { + mc += (ulong)mod.data[i] * (ulong)divEstimate; + t = resultNum.data[u + i]; + resultNum.data[u + i] -= (uint)mc; + mc >>= 32; + if (resultNum.data[u + i] > t) mc++; + i++; + } while (i < resultNum.length); + cc -= (uint)mc; + + if (cc != 0) { + + uint sc = 0, j = 0; + uint[] s = mod.data; + do { + uint a = s[j]; + if (((a += sc) < sc) | ((resultNum.data[u + j] -= a) > ~a)) sc = 1; + else sc = 0; + j++; + } while (j < resultNum.length); + cc -= sc; + } + while (resultNum >= mod) + Kernel.MinusEq(resultNum, mod); + } else { + while (resultNum >= mod) + Kernel.MinusEq(resultNum, mod); + } + } + } while (pos-- > 0); + + return resultNum; + } + + private BigInteger EvenModTwoPow (BigInteger exp) + { + exp.Normalize (); + uint [] wkspace = new uint [mod.length << 1 + 1]; + + BigInteger resultNum = new BigInteger (2, mod.length << 1 +1); + + uint value = exp.data [exp.length - 1]; + uint mask = 0x80000000; + + // Find the first bit of the exponent + while ((value & mask) == 0) + mask >>= 1; + + // + // We know that the first itr will make the val 2, + // so eat one bit of the exponent + // + mask >>= 1; + + uint wPos = exp.length - 1; + + do { + value = exp.data [wPos]; + do { + Kernel.SquarePositive (resultNum, ref wkspace); + if (resultNum.length >= mod.length) + BarrettReduction (resultNum); + + if ((value & mask) != 0) { + // + // resultNum = (resultNum * 2) % mod + // + + uint u = 0; + // + // Double + // + uint uu = u; + uint uuE = u + resultNum.length; + uint x, carry = 0; + while (uu < uuE) { + x = resultNum.data[uu]; + resultNum.data[uu] = (x << 1) | carry; + carry = x >> (32 - 1); + uu++; + } + + // subtraction inlined because we know it is square + if (carry != 0 || resultNum >= mod) { + uu = u; + uint c = 0; + uint[] s = mod.data; + uint i = 0; + do { + uint a = s[i]; + if (((a += c) < c) | ((resultNum.data[uu++] -= a) > ~a)) + c = 1; + else + c = 0; + i++; + } while (uu < uuE); + } + + } + } while ((mask >>= 1) > 0); + mask = 0x80000000; + } while (wPos-- > 0); + + return resultNum; + } + + private BigInteger OddModTwoPow (BigInteger exp) + { + + uint [] wkspace = new uint [mod.length << 1 + 1]; + + BigInteger resultNum = Montgomery.ToMont ((BigInteger)2, this.mod); + resultNum = new BigInteger (resultNum, mod.length << 1 +1); + + uint mPrime = Montgomery.Inverse (mod.data [0]); + + // + // TODO: eat small bits, the ones we can do with no modular reduction + // + uint pos = (uint)exp.bitCount () - 2; + + do { + Kernel.SquarePositive (resultNum, ref wkspace); + resultNum = Montgomery.Reduce (resultNum, mod, mPrime); + + if (exp.testBit(pos)) { + // + // resultNum = (resultNum * 2) % mod + // + + uint u = 0; + // + // Double + // + uint uu = u; + uint uuE = u + resultNum.length; + uint x, carry = 0; + while (uu < uuE) { + x = resultNum.data[uu]; + resultNum.data[uu] = (x << 1) | carry; + carry = x >> (32 - 1); + uu++; + } + + // subtraction inlined because we know it is square + if (carry != 0 || resultNum >= mod) { + uint s = 0; + uu = u; + uint c = 0; + uint ss = s; + do { + uint a = mod.data[ss++]; + if (((a += c) < c) | ((resultNum.data[uu++] -= a) > ~a)) + c = 1; + else + c = 0; + } while (uu < uuE); + } + } + } while (pos-- > 0); + + resultNum = Montgomery.Reduce (resultNum, mod, mPrime); + return resultNum; + } + + #endregion + } + + public sealed class Montgomery { + public static uint Inverse (uint n) + { + uint y = n, z; + + while ((z = n * y) != 1) + y *= 2 - z; + + return (uint)-y; + } + + public static BigInteger ToMont (BigInteger n, BigInteger m) + { + n.Normalize (); m.Normalize (); + + n <<= (int)m.length * 32; + n %= m; + return n; + } + + public static BigInteger Reduce(BigInteger n, BigInteger m, uint mPrime) + { + BigInteger A = n; + uint a = 0, mm = 0; + for (uint i = 0; i < m.length; i++) { + // The mod here is taken care of by the CPU, + // since the multiply will overflow. + uint u_i = A.data[a] * mPrime /* % 2^32 */; + + // + // A += u_i * m; + // A >>= 32 + // + + // mP = Position in mod + // aSP = the source of bits from a + // aDP = destination for bits + uint mP = mm, aSP = a, aDP = a; + + ulong c = (ulong)u_i * (ulong)m.data[mP++] + A.data[aSP++]; + c >>= 32; + uint j = 1; + + // Multiply and add + for (; j < m.length; j++) { + c += (ulong)u_i * (ulong)m.data[mP++] + A.data[aSP++]; + A.data[aDP++] = (uint)c; + c >>= 32; + } + + // Account for carry + // TODO: use a better loop here, we dont need the ulong stuff + for (; j < A.length; j++) { + c += A.data[aSP++]; + A.data[aDP++] = (uint)c; + c >>= 32; + if (c == 0) { j++; break; } + } + // Copy the rest + for (; j < A.length; j++) { + A.data[aDP++] = A.data[aSP++]; + } + + A.data[aDP++] = (uint)c; + } + + while (A.length > 1 && A.data[a + A.length - 1] == 0) A.length--; + + if (A >= m) Kernel.MinusEq(A, m); + + return A; + } + + public static BigInteger Reduce (BigInteger n, BigInteger m) + { + return Reduce (n, m, Inverse (m.data [0])); + } + } + + /// <summary> + /// Low level functions for the BigInteger + /// </summary> + private sealed class Kernel { + private Kernel() { } + #region Addition/Subtraction + + /// <summary> + /// Adds two numbers with the same sign. + /// </summary> + /// <param name="bi1">A BigInteger</param> + /// <param name="bi2">A BigInteger</param> + /// <returns>bi1 + bi2</returns> + public static BigInteger AddSameSign (BigInteger bi1, BigInteger bi2) + { + uint [] x, y; + uint yMax, xMax, i = 0; + + // x should be bigger + if (bi1.length < bi2.length) { + x = bi2.data; + xMax = bi2.length; + y = bi1.data; + yMax = bi1.length; + } else { + x = bi1.data; + xMax = bi1.length; + y = bi2.data; + yMax = bi2.length; + } + + BigInteger result = new BigInteger (Sign.Positive, xMax + 1); + + uint [] r = result.data; + + ulong sum = 0; + + // Add common parts of both numbers + do { + sum = ((ulong)x [i]) + ((ulong)y [i]) + sum; + r [i] = (uint)sum; + sum >>= 32; + } while (++i < yMax); + + // Copy remainder of longer number while carry propagation is required + bool carry = (sum != 0); + + if (carry) { + + if (i < xMax) { + do + carry = ((r [i] = x [i] + 1) == 0); + while (++i < xMax && carry); + } + + if (carry) { + r [i] = 1; + result.length = ++i; + return result; + } + } + + // Copy the rest + if (i < xMax) { + do + r [i] = x [i]; + while (++i < xMax); + } + + result.Normalize (); + return result; + } + + public static BigInteger Subtract (BigInteger big, BigInteger small) + { + BigInteger result = new BigInteger (Sign.Positive, big.length); + + uint [] r = result.data, b = big.data, s = small.data; + uint i = 0, c = 0; + + do { + + uint x = s [i]; + if (((x += c) < c) | ((r [i] = b [i] - x) > ~x)) + c = 1; + else + c = 0; + + } while (++i < small.length); + + if (i == big.length) goto fixup; + + if (c == 1) { + do + r [i] = b [i] - 1; + while (b [i++] == 0 && i < big.length); + + if (i == big.length) goto fixup; + } + + do + r [i] = b [i]; + while (++i < big.length); + + fixup: + + result.Normalize (); + return result; + } + + public static void MinusEq (BigInteger big, BigInteger small) + { + uint [] b = big.data, s = small.data; + uint i = 0, c = 0; + + do { + uint x = s [i]; + if (((x += c) < c) | ((b [i] -= x) > ~x)) + c = 1; + else + c = 0; + } while (++i < small.length); + + if (i == big.length) goto fixup; + + if (c == 1) { + do + b [i]--; + while (b [i++] == 0 && i < big.length); + } + + fixup: + + // Normalize length + while (big.length > 0 && big.data [big.length-1] == 0) big.length--; + + // Check for zero + if (big.length == 0) + big.length++; + + } + + public static void PlusEq (BigInteger bi1, BigInteger bi2) + { + uint [] x, y; + uint yMax, xMax, i = 0; + bool flag = false; + + // x should be bigger + if (bi1.length < bi2.length){ + flag = true; + x = bi2.data; + xMax = bi2.length; + y = bi1.data; + yMax = bi1.length; + } else { + x = bi1.data; + xMax = bi1.length; + y = bi2.data; + yMax = bi2.length; + } + + uint [] r = bi1.data; + + ulong sum = 0; + + // Add common parts of both numbers + do { + sum += ((ulong)x [i]) + ((ulong)y [i]); + r [i] = (uint)sum; + sum >>= 32; + } while (++i < yMax); + + // Copy remainder of longer number while carry propagation is required + bool carry = (sum != 0); + + if (carry){ + + if (i < xMax) { + do + carry = ((r [i] = x [i] + 1) == 0); + while (++i < xMax && carry); + } + + if (carry) { + r [i] = 1; + bi1.length = ++i; + return; + } + } + + // Copy the rest + if (flag && i < xMax - 1) { + do + r [i] = x [i]; + while (++i < xMax); + } + + bi1.length = xMax + 1; + bi1.Normalize (); + } + + #endregion + + #region Compare + + /// <summary> + /// Compares two BigInteger + /// </summary> + /// <param name="bi1">A BigInteger</param> + /// <param name="bi2">A BigInteger</param> + /// <returns>The sign of bi1 - bi2</returns> + public static Sign Compare (BigInteger bi1, BigInteger bi2) + { + // + // Step 1. Compare the lengths + // + uint l1 = bi1.length, l2 = bi2.length; + + while (l1 > 0 && bi1.data [l1-1] == 0) l1--; + while (l2 > 0 && bi2.data [l2-1] == 0) l2--; + + if (l1 == 0 && l2 == 0) return Sign.Zero; + + // bi1 len < bi2 len + if (l1 < l2) return Sign.Negative; + // bi1 len > bi2 len + else if (l1 > l2) return Sign.Positive; + + // + // Step 2. Compare the bits + // + + uint pos = l1 - 1; + + while (pos != 0 && bi1.data [pos] == bi2.data [pos]) pos--; + + if (bi1.data [pos] < bi2.data [pos]) + return Sign.Negative; + else if (bi1.data [pos] > bi2.data [pos]) + return Sign.Positive; + else + return Sign.Zero; + } + + #endregion + + #region Division + + #region Dword + + /// <summary> + /// Performs n / d and n % d in one operation. + /// </summary> + /// <param name="n">A BigInteger, upon exit this will hold n / d</param> + /// <param name="d">The divisor</param> + /// <returns>n % d</returns> + public static uint SingleByteDivideInPlace (BigInteger n, uint d) + { + ulong r = 0; + uint i = n.length; + + while (i-- > 0) { + r <<= 32; + r |= n.data [i]; + n.data [i] = (uint)(r / d); + r %= d; + } + n.Normalize (); + + return (uint)r; + } + + public static uint DwordMod (BigInteger n, uint d) + { + ulong r = 0; + uint i = n.length; + + while (i-- > 0) { + r <<= 32; + r |= n.data [i]; + r %= d; + } + + return (uint)r; + } + + public static BigInteger DwordDiv (BigInteger n, uint d) + { + BigInteger ret = new BigInteger (Sign.Positive, n.length); + + ulong r = 0; + uint i = n.length; + + while (i-- > 0) { + r <<= 32; + r |= n.data [i]; + ret.data [i] = (uint)(r / d); + r %= d; + } + ret.Normalize (); + + return ret; + } + + public static BigInteger [] DwordDivMod (BigInteger n, uint d) + { + BigInteger ret = new BigInteger (Sign.Positive , n.length); + + ulong r = 0; + uint i = n.length; + + while (i-- > 0) { + r <<= 32; + r |= n.data [i]; + ret.data [i] = (uint)(r / d); + r %= d; + } + ret.Normalize (); + + BigInteger rem = (uint)r; + + return new BigInteger [] {ret, rem}; + } + + #endregion + + #region BigNum + + public static BigInteger [] multiByteDivide (BigInteger bi1, BigInteger bi2) + { + if (Kernel.Compare (bi1, bi2) == Sign.Negative) + return new BigInteger [2] { 0, new BigInteger (bi1) }; + + bi1.Normalize (); bi2.Normalize (); + + if (bi2.length == 1) + return DwordDivMod (bi1, bi2.data [0]); + + uint remainderLen = bi1.length + 1; + int divisorLen = (int)bi2.length + 1; + + uint mask = 0x80000000; + uint val = bi2.data [bi2.length - 1]; + int shift = 0; + int resultPos = (int)bi1.length - (int)bi2.length; + + while (mask != 0 && (val & mask) == 0) { + shift++; mask >>= 1; + } + + BigInteger quot = new BigInteger (Sign.Positive, bi1.length - bi2.length + 1); + BigInteger rem = (bi1 << shift); + + uint [] remainder = rem.data; + + bi2 = bi2 << shift; + + int j = (int)(remainderLen - bi2.length); + int pos = (int)remainderLen - 1; + + uint firstDivisorByte = bi2.data [bi2.length-1]; + ulong secondDivisorByte = bi2.data [bi2.length-2]; + + while (j > 0) { + ulong dividend = ((ulong)remainder [pos] << 32) + (ulong)remainder [pos-1]; + + ulong q_hat = dividend / (ulong)firstDivisorByte; + ulong r_hat = dividend % (ulong)firstDivisorByte; + + do { + + if (q_hat == 0x100000000 || + (q_hat * secondDivisorByte) > ((r_hat << 32) + remainder [pos-2])) { + q_hat--; + r_hat += (ulong)firstDivisorByte; + + if (r_hat < 0x100000000) + continue; + } + break; + } while (true); + + // + // At this point, q_hat is either exact, or one too large + // (more likely to be exact) so, we attempt to multiply the + // divisor by q_hat, if we get a borrow, we just subtract + // one from q_hat and add the divisor back. + // + + uint t; + uint dPos = 0; + int nPos = pos - divisorLen + 1; + ulong mc = 0; + uint uint_q_hat = (uint)q_hat; + do { + mc += (ulong)bi2.data [dPos] * (ulong)uint_q_hat; + t = remainder [nPos]; + remainder [nPos] -= (uint)mc; + mc >>= 32; + if (remainder [nPos] > t) mc++; + dPos++; nPos++; + } while (dPos < divisorLen); + + nPos = pos - divisorLen + 1; + dPos = 0; + + // Overestimate + if (mc != 0) { + uint_q_hat--; + ulong sum = 0; + + do { + sum = ((ulong)remainder [nPos]) + ((ulong)bi2.data [dPos]) + sum; + remainder [nPos] = (uint)sum; + sum >>= 32; + dPos++; nPos++; + } while (dPos < divisorLen); + + } + + quot.data [resultPos--] = (uint)uint_q_hat; + + pos--; + j--; + } + + quot.Normalize (); + rem.Normalize (); + BigInteger [] ret = new BigInteger [2] { quot, rem }; + + if (shift != 0) + ret [1] >>= shift; + + return ret; + } + + #endregion + + #endregion + + #region Shift + public static BigInteger LeftShift (BigInteger bi, int n) + { + if (n == 0) return new BigInteger (bi, bi.length + 1); + + int w = n >> 5; + n &= ((1 << 5) - 1); + + BigInteger ret = new BigInteger (Sign.Positive, bi.length + 1 + (uint)w); + + uint i = 0, l = bi.length; + if (n != 0) { + uint x, carry = 0; + while (i < l) { + x = bi.data [i]; + ret.data [i + w] = (x << n) | carry; + carry = x >> (32 - n); + i++; + } + ret.data [i + w] = carry; + } else { + while (i < l) { + ret.data [i + w] = bi.data [i]; + i++; + } + } + + ret.Normalize (); + return ret; + } + + public static BigInteger RightShift (BigInteger bi, int n) + { + if (n == 0) return new BigInteger (bi); + + int w = n >> 5; + int s = n & ((1 << 5) - 1); + + BigInteger ret = new BigInteger (Sign.Positive, bi.length - (uint)w + 1); + uint l = (uint)ret.data.Length - 1; + + if (s != 0) { + + uint x, carry = 0; + + while (l-- > 0) { + x = bi.data [l + w]; + ret.data [l] = (x >> n) | carry; + carry = x << (32 - n); + } + } else { + while (l-- > 0) + ret.data [l] = bi.data [l + w]; + + } + ret.Normalize (); + return ret; + } + + #endregion + + #region Multiply + + public static BigInteger MultiplyByDword (BigInteger n, uint f) + { + BigInteger ret = new BigInteger (Sign.Positive, n.length + 1); + + uint i = 0; + ulong c = 0; + + do { + c += (ulong)n.data [i] * (ulong)f; + ret.data [i] = (uint)c; + c >>= 32; + } while (++i < n.length); + ret.data [i] = (uint)c; + ret.Normalize (); + return ret; + + } + + /// <summary> + /// Multiplies the data in x [xOffset:xOffset+xLen] by + /// y [yOffset:yOffset+yLen] and puts it into + /// d [dOffset:dOffset+xLen+yLen]. + /// </summary> + public static void Multiply(uint[] x, uint xOffset, uint xLen, uint[] y, uint yOffset, uint yLen, uint[] d, uint dOffset) + { + uint xx = 0, yy = 0, dd = 0; + uint xP = xx + xOffset, + xE = xP + xLen, + yB = yy + yOffset, + yE = yB + yLen, + dB = dd + dOffset; + + for (; xP < xE; xP++, dB++) { + + if (x[xP] == 0) continue; + + ulong mcarry = 0; + + uint dP = dB; + for (uint yP = yB; yP < yE; yP++, dP++) { + mcarry += ((ulong)x[xP] * (ulong)y[yP]) + (ulong)d[dP]; + + d[dP] = (uint)mcarry; + mcarry >>= 32; + } + + if (mcarry != 0) + d[dP] = (uint)mcarry; + } + } + + /// <summary> + /// Multiplies the data in x [xOffset:xOffset+xLen] by + /// y [yOffset:yOffset+yLen] and puts the low mod words into + /// d [dOffset:dOffset+mod]. + /// </summary> + public static void MultiplyMod2p32pmod(uint[] x, int xOffset, int xLen, uint[] y, int yOffest, int yLen, uint[] d, int dOffset, int mod) + { + uint xx = 0, yy = 0, dd = 0; + uint xP = (uint)(xx + xOffset), + xE = (uint)(xP + xLen), + yB = (uint)(yy + yOffest), + yE = (uint)(yB + yLen), + dB = (uint)(dd + dOffset), + dE = (uint)(dB + mod); + + for (; xP < xE; xP++, dB++) + { + + if (x[xP] == 0) continue; + + ulong mcarry = 0; + uint dP = dB; + for (uint yP = yB; yP < yE && dP < dE; yP++, dP++) + { + mcarry += ((ulong)x[xP] * (ulong)y[yP]) + (ulong)d[dP]; + + d[dP] = (uint)mcarry; + mcarry >>= 32; + } + + if (mcarry != 0 && dP < dE) + d[dP] = (uint)mcarry; + } + } + + public static void SquarePositive(BigInteger bi, ref uint[] wkSpace) { + uint[] t = wkSpace; + wkSpace = bi.data; + uint[] d = bi.data; + uint dl = bi.length; + bi.data = t; + + uint dd = 0, tt = 0; + + uint ttE = (uint)t.Length; + // Clear the dest + for (uint ttt = tt; ttt < ttE; ttt++) + t[ttt] = 0; + + uint dP = dd, tP = tt; + + for (uint i = 0; i < dl; i++, dP++) { + if (d[dP] == 0) + continue; + + ulong mcarry = 0; + uint bi1val = d[dP]; + + uint dP2 = dP + 1, tP2 = tP + 2 * i + 1; + + for (uint j = i + 1; j < dl; j++, tP2++, dP2++) { + // k = i + j + mcarry += ((ulong)bi1val * (ulong)d[dP2]) + t[tP2]; + + t[tP2] = (uint)mcarry; + mcarry >>= 32; + } + + if (mcarry != 0) + t[tP2] = (uint)mcarry; + } + + // Double t. Inlined for speed. + + tP = tt; + + uint x, carry = 0; + while (tP < ttE) { + x = t[tP]; + t[tP] = (x << 1) | carry; + carry = x >> (32 - 1); + tP++; + } + if (carry != 0) t[tP] = carry; + + // Add in the diagnals + + dP = dd; + tP = tt; + for (uint dE = dP + dl; (dP < dE); dP++, tP++) { + ulong val = (ulong)d[dP] * (ulong)d[dP] + t[tP]; + t[tP] = (uint)val; + val >>= 32; + t[(++tP)] += (uint)val; + if (t[tP] < (uint)val) { + uint tP3 = tP; + // Account for the first carry + (t[++tP3])++; + + // Keep adding until no carry + while ((t[tP3++]) == 0x0) + (t[tP3])++; + } + + } + + bi.length <<= 1; + + // Normalize length + while (t[tt + bi.length - 1] == 0 && bi.length > 1) bi.length--; + + } +#if UNUSED + public static bool Double (uint [] u, int l) + { + uint x, carry = 0; + uint i = 0; + while (i < l) { + x = u [i]; + u [i] = (x << 1) | carry; + carry = x >> (32 - 1); + i++; + } + if (carry != 0) u [l] = carry; + return carry != 0; + } +#endif + #endregion + + #region Number Theory + + public static BigInteger gcd (BigInteger a, BigInteger b) + { + BigInteger x = a; + BigInteger y = b; + + BigInteger g = y; + + while (x.length > 1) { + g = x; + x = y % x; + y = g; + + } + if (x == 0) return g; + + // TODO: should we have something here if we can convert to long? + + // + // Now we can just do it with single precision. I am using the binary gcd method, + // as it should be faster. + // + + uint yy = x.data [0]; + uint xx = y % yy; + + int t = 0; + + while (((xx | yy) & 1) == 0) { + xx >>= 1; yy >>= 1; t++; + } + while (xx != 0) { + while ((xx & 1) == 0) xx >>= 1; + while ((yy & 1) == 0) yy >>= 1; + if (xx >= yy) + xx = (xx - yy) >> 1; + else + yy = (yy - xx) >> 1; + } + + return yy << t; + } + + public static uint modInverse (BigInteger bi, uint modulus) + { + uint a = modulus, b = bi % modulus; + uint p0 = 0, p1 = 1; + + while (b != 0) { + if (b == 1) + return p1; + p0 += (a / b) * p1; + a %= b; + + if (a == 0) + break; + if (a == 1) + return modulus-p0; + + p1 += (b / a) * p0; + b %= a; + + } + return 0; + } + + public static BigInteger modInverse (BigInteger bi, BigInteger modulus) + { + if (modulus.length == 1) return modInverse (bi, modulus.data [0]); + + BigInteger [] p = { 0, 1 }; + BigInteger [] q = new BigInteger [2]; // quotients + BigInteger [] r = { 0, 0 }; // remainders + + int step = 0; + + BigInteger a = modulus; + BigInteger b = bi; + + ModulusRing mr = new ModulusRing (modulus); + + while (b != 0) { + + if (step > 1) { + + BigInteger pval = mr.Difference (p [0], p [1] * q [0]); + p [0] = p [1]; p [1] = pval; + } + + BigInteger [] divret = multiByteDivide (a, b); + + q [0] = q [1]; q [1] = divret [0]; + r [0] = r [1]; r [1] = divret [1]; + a = b; + b = divret [1]; + + step++; + } + + if (r [0] != 1) + throw (new ArithmeticException ("No inverse!")); + + return mr.Difference (p [0], p [1] * q [0]); + + } + #endregion + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/ConfidenceFactor.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/ConfidenceFactor.cs new file mode 100644 index 0000000..fd0747d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/ConfidenceFactor.cs @@ -0,0 +1,44 @@ +// <auto-generated/> + +// +// Mono.Math.Prime.ConfidenceFactor.cs - Confidence factor for prime generation +// +// Authors: +// Ben Maurer +// +// Copyright (c) 2003 Ben Maurer. All rights reserved +// + +using System; + +namespace Mono.Math.Prime { + /// <summary> + /// A factor of confidence. + /// </summary> + internal enum ConfidenceFactor { + /// <summary> + /// Only suitable for development use, probability of failure may be greater than 1/2^20. + /// </summary> + ExtraLow, + /// <summary> + /// Suitable only for transactions which do not require forward secrecy. Probability of failure about 1/2^40 + /// </summary> + Low, + /// <summary> + /// Designed for production use. Probability of failure about 1/2^80. + /// </summary> + Medium, + /// <summary> + /// Suitable for sensitive data. Probability of failure about 1/2^160. + /// </summary> + High, + /// <summary> + /// Use only if you have lots of time! Probability of failure about 1/2^320. + /// </summary> + ExtraHigh, + /// <summary> + /// Only use methods which generate provable primes. Not yet implemented. + /// </summary> + Provable + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/NextPrimeFinder.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/NextPrimeFinder.cs new file mode 100644 index 0000000..19433f2 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/NextPrimeFinder.cs @@ -0,0 +1,29 @@ +// <auto-generated /> +// +// Mono.Math.Prime.Generator.NextPrimeFinder.cs - Prime Generator +// +// Authors: +// Ben Maurer +// +// Copyright (c) 2003 Ben Maurer. All rights reserved +// + +using System; + +namespace Mono.Math.Prime.Generator { + + /// <summary> + /// Finds the next prime after a given number. + /// </summary> + //[CLSCompliant(false)] + internal class NextPrimeFinder : SequentialSearchPrimeGeneratorBase { + + protected override BigInteger GenerateSearchBase (int bits, object Context) + { + if (Context == null) throw new ArgumentNullException ("Context"); + BigInteger ret = new BigInteger ((BigInteger)Context); + ret.setBit (0); + return ret; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/PrimalityTests.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/PrimalityTests.cs new file mode 100644 index 0000000..b2ddc74 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/PrimalityTests.cs @@ -0,0 +1,177 @@ +// <auto-generated /> +// +// Mono.Math.Prime.PrimalityTests.cs - Test for primality +// +// Authors: +// Ben Maurer +// +// Copyright (c) 2003 Ben Maurer. All rights reserved +// + +using System; +using System.Security.Cryptography; + +namespace Mono.Math.Prime { + + //[CLSCompliant(false)] + internal delegate bool PrimalityTest (BigInteger bi, ConfidenceFactor confidence); + + //[CLSCompliant(false)] + internal sealed class PrimalityTests { + + #region SPP Test + + private static int GetSPPRounds (BigInteger bi, ConfidenceFactor confidence) + { + int bc = bi.bitCount(); + + int Rounds; + + // Data from HAC, 4.49 + if (bc <= 100 ) Rounds = 27; + else if (bc <= 150 ) Rounds = 18; + else if (bc <= 200 ) Rounds = 15; + else if (bc <= 250 ) Rounds = 12; + else if (bc <= 300 ) Rounds = 9; + else if (bc <= 350 ) Rounds = 8; + else if (bc <= 400 ) Rounds = 7; + else if (bc <= 500 ) Rounds = 6; + else if (bc <= 600 ) Rounds = 5; + else if (bc <= 800 ) Rounds = 4; + else if (bc <= 1250) Rounds = 3; + else Rounds = 2; + + switch (confidence) { + case ConfidenceFactor.ExtraLow: + Rounds >>= 2; + return Rounds != 0 ? Rounds : 1; + case ConfidenceFactor.Low: + Rounds >>= 1; + return Rounds != 0 ? Rounds : 1; + case ConfidenceFactor.Medium: + return Rounds; + case ConfidenceFactor.High: + return Rounds <<= 1; + case ConfidenceFactor.ExtraHigh: + return Rounds <<= 2; + case ConfidenceFactor.Provable: + throw new Exception ("The Rabin-Miller test can not be executed in a way such that its results are provable"); + default: + throw new ArgumentOutOfRangeException ("confidence"); + } + } + + /// <summary> + /// Probabilistic prime test based on Rabin-Miller's test + /// </summary> + /// <param name="bi" type="BigInteger.BigInteger"> + /// <para> + /// The number to test. + /// </para> + /// </param> + /// <param name="confidence" type="int"> + /// <para> + /// The number of chosen bases. The test has at least a + /// 1/4^confidence chance of falsely returning True. + /// </para> + /// </param> + /// <returns> + /// <para> + /// True if "this" is a strong pseudoprime to randomly chosen bases. + /// </para> + /// <para> + /// False if "this" is definitely NOT prime. + /// </para> + /// </returns> + public static bool RabinMillerTest (BigInteger bi, ConfidenceFactor confidence) + { + int Rounds = GetSPPRounds (bi, confidence); + + // calculate values of s and t + BigInteger p_sub1 = bi - 1; + int s = p_sub1.LowestSetBit (); + + BigInteger t = p_sub1 >> s; + + int bits = bi.bitCount (); + BigInteger a = null; + RandomNumberGenerator rng = RandomNumberGenerator.Create (); + BigInteger.ModulusRing mr = new BigInteger.ModulusRing (bi); + + for (int round = 0; round < Rounds; round++) { + while (true) { // generate a < n + a = BigInteger.genRandom (bits, rng); + + // make sure "a" is not 0 + if (a > 1 && a < bi) + break; + } + + if (a.gcd (bi) != 1) return false; + + BigInteger b = mr.Pow (a, t); + + if (b == 1) continue; // a^t mod p = 1 + + bool result = false; + for (int j = 0; j < s; j++) { + + if (b == p_sub1) { // a^((2^j)*t) mod p = p-1 for some 0 <= j <= s-1 + result = true; + break; + } + + b = (b * b) % bi; + } + + if (result == false) + return false; + } + return true; + } + + public static bool SmallPrimeSppTest (BigInteger bi, ConfidenceFactor confidence) + { + int Rounds = GetSPPRounds (bi, confidence); + + // calculate values of s and t + BigInteger p_sub1 = bi - 1; + int s = p_sub1.LowestSetBit (); + + BigInteger t = p_sub1 >> s; + + + BigInteger.ModulusRing mr = new BigInteger.ModulusRing (bi); + + for (int round = 0; round < Rounds; round++) { + + BigInteger b = mr.Pow (BigInteger.smallPrimes [round], t); + + if (b == 1) continue; // a^t mod p = 1 + + bool result = false; + for (int j = 0; j < s; j++) { + + if (b == p_sub1) { // a^((2^j)*t) mod p = p-1 for some 0 <= j <= s-1 + result = true; + break; + } + + b = (b * b) % bi; + } + + if (result == false) + return false; + } + return true; + + } + + #endregion + + + // TODO: Implement the Lucus test + // TODO: Implement other new primality tests + // TODO: Implement primality proving + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/PrimeGeneratorBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/PrimeGeneratorBase.cs new file mode 100644 index 0000000..12b6a69 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/PrimeGeneratorBase.cs @@ -0,0 +1,57 @@ +// <auto-generated /> +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +namespace Mono.Math.Prime.Generator { + + //[CLSCompliant(false)] + internal abstract class PrimeGeneratorBase { + + public virtual ConfidenceFactor Confidence { + get { + return ConfidenceFactor.Medium; + } + } + + public virtual Prime.PrimalityTest PrimalityTest { + get { + return new Prime.PrimalityTest (PrimalityTests.SmallPrimeSppTest); + } + } + + public virtual int TrialDivisionBounds { + get { return 4000; } + } + + /// <summary> + /// Performs primality tests on bi, assumes trial division has been done. + /// </summary> + /// <param name="bi">A BigInteger that has been subjected to and passed trial division</param> + /// <returns>False if bi is composite, true if it may be prime.</returns> + /// <remarks>The speed of this method is dependent on Confidence</remarks> + protected bool PostTrialDivisionTests (BigInteger bi) + { + return PrimalityTest (bi, this.Confidence); + } + + public abstract BigInteger GenerateNewPrime (int bits); + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/SequentialSearchPrimeGeneratorBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/SequentialSearchPrimeGeneratorBase.cs new file mode 100644 index 0000000..a017ee4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellman/mono/SequentialSearchPrimeGeneratorBase.cs @@ -0,0 +1,100 @@ +// <auto-generated /> + +// +// Mono.Math.Prime.Generator.SequentialSearchPrimeGeneratorBase.cs - Prime Generator +// +// Authors: +// Ben Maurer +// +// Copyright (c) 2003 Ben Maurer. All rights reserved +// + +using System; +using Mono.Math.Prime; + +namespace Mono.Math.Prime.Generator { + + //[CLSCompliant(false)] + internal class SequentialSearchPrimeGeneratorBase : PrimeGeneratorBase { + + protected virtual BigInteger GenerateSearchBase (int bits, object Context) + { + BigInteger ret = BigInteger.genRandom (bits); + ret.setBit (0); + return ret; + } + + + public override BigInteger GenerateNewPrime (int bits) + { + return GenerateNewPrime (bits, null); + } + + + public virtual BigInteger GenerateNewPrime (int bits, object Context) + { + // + // STEP 1. Find a place to do a sequential search + // + BigInteger curVal = GenerateSearchBase (bits, Context); + + const uint primeProd1 = 3u* 5u * 7u * 11u * 13u * 17u * 19u * 23u * 29u; + + uint pMod1 = curVal % primeProd1; + + int DivisionBound = TrialDivisionBounds; + uint[] SmallPrimes = BigInteger.smallPrimes; + PrimalityTest PostTrialDivisionTest = this.PrimalityTest; + // + // STEP 2. Search for primes + // + while (true) { + + // + // STEP 2.1 Sieve out numbers divisible by the first 9 primes + // + if (pMod1 % 3 == 0) goto biNotPrime; + if (pMod1 % 5 == 0) goto biNotPrime; + if (pMod1 % 7 == 0) goto biNotPrime; + if (pMod1 % 11 == 0) goto biNotPrime; + if (pMod1 % 13 == 0) goto biNotPrime; + if (pMod1 % 17 == 0) goto biNotPrime; + if (pMod1 % 19 == 0) goto biNotPrime; + if (pMod1 % 23 == 0) goto biNotPrime; + if (pMod1 % 29 == 0) goto biNotPrime; + + // + // STEP 2.2 Sieve out all numbers divisible by the primes <= DivisionBound + // + for (int p = 9; p < SmallPrimes.Length && SmallPrimes [p] <= DivisionBound; p++) { + if (curVal % SmallPrimes [p] == 0) + goto biNotPrime; + } + + // + // STEP 2.3 Is the potential prime acceptable? + // + if (!IsPrimeAcceptable (curVal, Context)) goto biNotPrime; + + // + // STEP 2.4 Filter out all primes that pass this step with a primality test + // + if (PrimalityTest (curVal, Confidence)) return curVal; + + + // + // STEP 2.4 + // + biNotPrime: + pMod1 += 2; + if (pMod1 >= primeProd1) pMod1 -= primeProd1; + curVal.Incr2 (); + } + } + + protected virtual bool IsPrimeAcceptable (BigInteger bi, object Context) + { + return true; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellmanUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellmanUtilities.cs new file mode 100644 index 0000000..249f1f3 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/DiffieHellmanUtilities.cs @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------- +// <copyright file="DiffieHellmanUtilities.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.Globalization; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using Org.Mentalis.Security.Cryptography; + + /// <summary> + /// Diffie-Hellman encryption methods used by both the relying party and provider. + /// </summary> + internal class DiffieHellmanUtilities { + /// <summary> + /// An array of known Diffie Hellman sessions, sorted by decreasing hash size. + /// </summary> + private static DHSha[] diffieHellmanSessionTypes = new List<DHSha> { + new DHSha(new SHA512Managed(), protocol => protocol.Args.SessionType.DH_SHA512), + new DHSha(new SHA384Managed(), protocol => protocol.Args.SessionType.DH_SHA384), + new DHSha(new SHA256Managed(), protocol => protocol.Args.SessionType.DH_SHA256), + new DHSha(new SHA1Managed(), protocol => protocol.Args.SessionType.DH_SHA1), + } .ToArray(); + + /// <summary> + /// Finds the hashing algorithm to use given an openid.session_type value. + /// </summary> + /// <param name="protocol">The protocol version of the message that named the session_type to be used.</param> + /// <param name="sessionType">The value of the openid.session_type parameter.</param> + /// <returns>The hashing algorithm to use.</returns> + /// <exception cref="ProtocolException">Thrown if no match could be found for the given <paramref name="sessionType"/>.</exception> + public static HashAlgorithm Lookup(Protocol protocol, string sessionType) { + Contract.Requires<ArgumentNullException>(protocol != null); + Contract.Requires<ArgumentNullException>(sessionType != null); + + // We COULD use just First instead of FirstOrDefault, but we want to throw ProtocolException instead of InvalidOperationException. + DHSha match = diffieHellmanSessionTypes.FirstOrDefault(dhsha => String.Equals(dhsha.GetName(protocol), sessionType, StringComparison.Ordinal)); + ErrorUtilities.VerifyProtocol(match != null, OpenIdStrings.NoSessionTypeFound, sessionType, protocol.Version); + return match.Algorithm; + } + + /// <summary> + /// Looks up the value to be used for the openid.session_type parameter. + /// </summary> + /// <param name="protocol">The protocol version that is to be used.</param> + /// <param name="hashSizeInBits">The hash size (in bits) that the DH session must have.</param> + /// <returns>The value to be used for the openid.session_type parameter, or null if no match was found.</returns> + internal static string GetNameForSize(Protocol protocol, int hashSizeInBits) { + Contract.Requires<ArgumentNullException>(protocol != null); + DHSha match = diffieHellmanSessionTypes.FirstOrDefault(dhsha => dhsha.Algorithm.HashSize == hashSizeInBits); + return match != null ? match.GetName(protocol) : null; + } + + /// <summary> + /// Encrypts/decrypts a shared secret. + /// </summary> + /// <param name="hasher">The hashing algorithm that is agreed by both parties to use as part of the secret exchange.</param> + /// <param name="dh"> + /// If the secret is being encrypted, this is the new Diffie Hellman object to use. + /// If the secret is being decrypted, this must be the same Diffie Hellman object used to send the original request message. + /// </param> + /// <param name="remotePublicKey">The public key of the remote party.</param> + /// <param name="plainOrEncryptedSecret">The secret to encode, or the encoded secret. Whichever one is given will generate the opposite in the return value.</param> + /// <returns> + /// The encrypted version of the secret if the secret itself was given in <paramref name="remotePublicKey"/>. + /// The secret itself if the encrypted version of the secret was given in <paramref name="remotePublicKey"/>. + /// </returns> + internal static byte[] SHAHashXorSecret(HashAlgorithm hasher, DiffieHellman dh, byte[] remotePublicKey, byte[] plainOrEncryptedSecret) { + Contract.Requires<ArgumentNullException>(hasher != null); + Contract.Requires<ArgumentNullException>(dh != null); + Contract.Requires<ArgumentNullException>(remotePublicKey != null); + Contract.Requires<ArgumentNullException>(plainOrEncryptedSecret != null); + + byte[] sharedBlock = dh.DecryptKeyExchange(remotePublicKey); + byte[] sharedBlockHash = hasher.ComputeHash(EnsurePositive(sharedBlock)); + ErrorUtilities.VerifyProtocol(sharedBlockHash.Length == plainOrEncryptedSecret.Length, OpenIdStrings.AssociationSecretHashLengthMismatch, plainOrEncryptedSecret.Length, sharedBlockHash.Length); + + byte[] secret = new byte[plainOrEncryptedSecret.Length]; + for (int i = 0; i < plainOrEncryptedSecret.Length; i++) { + secret[i] = (byte)(plainOrEncryptedSecret[i] ^ sharedBlockHash[i]); + } + return secret; + } + + /// <summary> + /// Ensures that the big integer represented by a given series of bytes + /// is a positive integer. + /// </summary> + /// <param name="inputBytes">The bytes that make up the big integer.</param> + /// <returns> + /// A byte array (possibly new if a change was required) whose + /// integer is guaranteed to be positive. + /// </returns> + /// <remarks> + /// This is to be consistent with OpenID spec section 4.2. + /// </remarks> + internal static byte[] EnsurePositive(byte[] inputBytes) { + Contract.Requires<ArgumentNullException>(inputBytes != null); + if (inputBytes.Length == 0) { + throw new ArgumentException(MessagingStrings.UnexpectedEmptyArray, "inputBytes"); + } + + int i = (int)inputBytes[0]; + if (i > 127) { + byte[] nowPositive = new byte[inputBytes.Length + 1]; + nowPositive[0] = 0; + inputBytes.CopyTo(nowPositive, 1); + return nowPositive; + } + + return inputBytes; + } + + /// <summary> + /// Provides access to a Diffie-Hellman session algorithm and its name. + /// </summary> + private class DHSha { + /// <summary> + /// Initializes a new instance of the <see cref="DHSha"/> class. + /// </summary> + /// <param name="algorithm">The hashing algorithm used in this particular Diffie-Hellman session type.</param> + /// <param name="getName">A function that will return the value of the openid.session_type parameter for a given version of OpenID.</param> + public DHSha(HashAlgorithm algorithm, Func<Protocol, string> getName) { + Contract.Requires<ArgumentNullException>(algorithm != null); + Contract.Requires<ArgumentNullException>(getName != null); + + this.GetName = getName; + this.Algorithm = algorithm; + } + + /// <summary> + /// Gets the function that will return the value of the openid.session_type parameter for a given version of OpenID. + /// </summary> + internal Func<Protocol, string> GetName { get; private set; } + + /// <summary> + /// Gets the hashing algorithm used in this particular Diffie-Hellman session type + /// </summary> + internal HashAlgorithm Algorithm { get; private set; } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs new file mode 100644 index 0000000..0a84266 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AliasManager.cs @@ -0,0 +1,187 @@ +//----------------------------------------------------------------------- +// <copyright file="AliasManager.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; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Manages a fast, two-way mapping between type URIs and their aliases. + /// </summary> + internal class AliasManager { + /// <summary> + /// The format of auto-generated aliases. + /// </summary> + private const string AliasFormat = "alias{0}"; + + /// <summary> + /// Tracks extension Type URIs and aliases assigned to them. + /// </summary> + private Dictionary<string, string> typeUriToAliasMap = new Dictionary<string, string>(); + + /// <summary> + /// Tracks extension aliases and Type URIs assigned to them. + /// </summary> + private Dictionary<string, string> aliasToTypeUriMap = new Dictionary<string, string>(); + + /// <summary> + /// Gets the aliases that have been set. + /// </summary> + public IEnumerable<string> Aliases { + get { return this.aliasToTypeUriMap.Keys; } + } + + /// <summary> + /// Gets an alias assigned for a given Type URI. A new alias is assigned if necessary. + /// </summary> + /// <param name="typeUri">The type URI.</param> + /// <returns>The alias assigned to this type URI. Never null.</returns> + public string GetAlias(string typeUri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + string alias; + return this.typeUriToAliasMap.TryGetValue(typeUri, out alias) ? alias : this.AssignNewAlias(typeUri); + } + + /// <summary> + /// Sets an alias and the value that will be returned by <see cref="ResolveAlias"/>. + /// </summary> + /// <param name="alias">The alias.</param> + /// <param name="typeUri">The type URI.</param> + public void SetAlias(string alias, string typeUri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(alias)); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + this.aliasToTypeUriMap.Add(alias, typeUri); + this.typeUriToAliasMap.Add(typeUri, alias); + } + + /// <summary> + /// Takes a sequence of type URIs and assigns aliases for all of them. + /// </summary> + /// <param name="typeUris">The type URIs to create aliases for.</param> + /// <param name="preferredTypeUriToAliases">An optional dictionary of URI/alias pairs that suggest preferred aliases to use if available for certain type URIs.</param> + public void AssignAliases(IEnumerable<string> typeUris, IDictionary<string, string> preferredTypeUriToAliases) { + Contract.Requires<ArgumentNullException>(typeUris != null); + + // First go through the actually used type URIs and see which ones have matching preferred aliases. + if (preferredTypeUriToAliases != null) { + foreach (string typeUri in typeUris) { + if (this.typeUriToAliasMap.ContainsKey(typeUri)) { + // this Type URI is already mapped to an alias. + continue; + } + + string preferredAlias; + if (preferredTypeUriToAliases.TryGetValue(typeUri, out preferredAlias) && !this.IsAliasUsed(preferredAlias)) { + this.SetAlias(preferredAlias, typeUri); + } + } + } + + // Now go through the whole list again and assign whatever is left now that the preferred ones + // have gotten their picks where available. + foreach (string typeUri in typeUris) { + if (this.typeUriToAliasMap.ContainsKey(typeUri)) { + // this Type URI is already mapped to an alias. + continue; + } + + this.AssignNewAlias(typeUri); + } + } + + /// <summary> + /// Sets up aliases for any Type URIs in a dictionary that do not yet have aliases defined, + /// and where the given preferred alias is still available. + /// </summary> + /// <param name="preferredTypeUriToAliases">A dictionary of type URI keys and alias values.</param> + public void SetPreferredAliasesWhereNotSet(IDictionary<string, string> preferredTypeUriToAliases) { + Contract.Requires<ArgumentNullException>(preferredTypeUriToAliases != null); + + foreach (var pair in preferredTypeUriToAliases) { + if (this.typeUriToAliasMap.ContainsKey(pair.Key)) { + // type URI is already mapped + continue; + } + + if (this.aliasToTypeUriMap.ContainsKey(pair.Value)) { + // alias is already mapped + continue; + } + + // The type URI and alias are as yet unset, so go ahead and assign them. + this.SetAlias(pair.Value, pair.Key); + } + } + + /// <summary> + /// Gets the Type Uri encoded by a given alias. + /// </summary> + /// <param name="alias">The alias.</param> + /// <returns>The Type URI.</returns> + /// <exception cref="ArgumentOutOfRangeException">Thrown if the given alias does not have a matching TypeURI.</exception> + public string ResolveAlias(string alias) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(alias)); + string typeUri = this.TryResolveAlias(alias); + if (typeUri == null) { + throw new ArgumentOutOfRangeException("alias"); + } + return typeUri; + } + + /// <summary> + /// Gets the Type Uri encoded by a given alias. + /// </summary> + /// <param name="alias">The alias.</param> + /// <returns>The Type URI for the given alias, or null if none for that alias exist.</returns> + public string TryResolveAlias(string alias) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(alias)); + string typeUri = null; + this.aliasToTypeUriMap.TryGetValue(alias, out typeUri); + return typeUri; + } + + /// <summary> + /// Returns a value indicating whether an alias has already been assigned to a type URI. + /// </summary> + /// <param name="alias">The alias in question.</param> + /// <returns>True if the alias has already been assigned. False otherwise.</returns> + public bool IsAliasUsed(string alias) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(alias)); + return this.aliasToTypeUriMap.ContainsKey(alias); + } + + /// <summary> + /// Determines whether a given TypeURI has an associated alias assigned to it. + /// </summary> + /// <param name="typeUri">The type URI.</param> + /// <returns> + /// <c>true</c> if the given type URI already has an alias assigned; <c>false</c> otherwise. + /// </returns> + public bool IsAliasAssignedTo(string typeUri) { + Contract.Requires<ArgumentNullException>(typeUri != null); + return this.typeUriToAliasMap.ContainsKey(typeUri); + } + + /// <summary> + /// Assigns a new alias to a given Type URI. + /// </summary> + /// <param name="typeUri">The type URI to assign a new alias to.</param> + /// <returns>The newly generated alias.</returns> + private string AssignNewAlias(string typeUri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + ErrorUtilities.VerifyInternal(!this.typeUriToAliasMap.ContainsKey(typeUri), "Oops! This type URI already has an alias!"); + string alias = string.Format(CultureInfo.InvariantCulture, AliasFormat, this.typeUriToAliasMap.Count + 1); + this.typeUriToAliasMap.Add(typeUri, alias); + this.aliasToTypeUriMap.Add(alias, typeUri); + return alias; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs new file mode 100644 index 0000000..decd296 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXAttributeFormats.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------- +// <copyright file="AXAttributeFormats.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + + /// <summary> + /// The various Type URI formats an AX attribute may use by various remote parties. + /// </summary> + [Flags] + public enum AXAttributeFormats { + /// <summary> + /// No attribute format. + /// </summary> + None = 0x0, + + /// <summary> + /// AX attributes should use the Type URI format starting with <c>http://axschema.org/</c>. + /// </summary> + AXSchemaOrg = 0x1, + + /// <summary> + /// AX attributes should use the Type URI format starting with <c>http://schema.openid.net/</c>. + /// </summary> + SchemaOpenIdNet = 0x2, + + /// <summary> + /// AX attributes should use the Type URI format starting with <c>http://openid.net/schema/</c>. + /// </summary> + OpenIdNetSchema = 0x4, + + /// <summary> + /// All known schemas. + /// </summary> + All = AXSchemaOrg | SchemaOpenIdNet | OpenIdNetSchema, + + /// <summary> + /// The most common schemas. + /// </summary> + Common = AXSchemaOrg | SchemaOpenIdNet, + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs new file mode 100644 index 0000000..2b947f7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AXUtilities.cs @@ -0,0 +1,144 @@ +//----------------------------------------------------------------------- +// <copyright file="AXUtilities.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Helper methods shared by multiple messages in the Attribute Exchange extension. + /// </summary> + public static class AXUtilities { + /// <summary> + /// Adds a request for an attribute considering it 'required'. + /// </summary> + /// <param name="collection">The attribute request collection.</param> + /// <param name="typeUri">The type URI of the required attribute.</param> + public static void AddRequired(this ICollection<AttributeRequest> collection, string typeUri) { + Contract.Requires<ArgumentNullException>(collection != null); + collection.Add(new AttributeRequest(typeUri, true)); + } + + /// <summary> + /// Adds a request for an attribute without considering it 'required'. + /// </summary> + /// <param name="collection">The attribute request collection.</param> + /// <param name="typeUri">The type URI of the requested attribute.</param> + public static void AddOptional(this ICollection<AttributeRequest> collection, string typeUri) { + Contract.Requires<ArgumentNullException>(collection != null); + collection.Add(new AttributeRequest(typeUri, false)); + } + + /// <summary> + /// Adds a given attribute with one or more values to the request for storage. + /// Applicable to Relying Parties only. + /// </summary> + /// <param name="collection">The collection of <see cref="AttributeValues"/> to add to.</param> + /// <param name="typeUri">The type URI of the attribute.</param> + /// <param name="values">The attribute values.</param> + public static void Add(this ICollection<AttributeValues> collection, string typeUri, params string[] values) { + Contract.Requires<ArgumentNullException>(collection != null); + collection.Add(new AttributeValues(typeUri, values)); + } + + /// <summary> + /// Serializes a set of attribute values to a dictionary of fields to send in the message. + /// </summary> + /// <param name="fields">The dictionary to fill with serialized attributes.</param> + /// <param name="attributes">The attributes.</param> + internal static void SerializeAttributes(IDictionary<string, string> fields, IEnumerable<AttributeValues> attributes) { + Contract.Requires<ArgumentNullException>(fields != null); + Contract.Requires<ArgumentNullException>(attributes != null); + + AliasManager aliasManager = new AliasManager(); + foreach (var att in attributes) { + string alias = aliasManager.GetAlias(att.TypeUri); + fields.Add("type." + alias, att.TypeUri); + if (att.Values == null) { + continue; + } + if (att.Values.Count != 1) { + fields.Add("count." + alias, att.Values.Count.ToString(CultureInfo.InvariantCulture)); + for (int i = 0; i < att.Values.Count; i++) { + fields.Add(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i + 1), att.Values[i]); + } + } else { + fields.Add("value." + alias, att.Values[0]); + } + } + } + + /// <summary> + /// Deserializes attribute values from an incoming set of message data. + /// </summary> + /// <param name="fields">The data coming in with the message.</param> + /// <returns>The attribute values found in the message.</returns> + internal static IEnumerable<AttributeValues> DeserializeAttributes(IDictionary<string, string> fields) { + AliasManager aliasManager = ParseAliases(fields); + foreach (string alias in aliasManager.Aliases) { + AttributeValues att = new AttributeValues(aliasManager.ResolveAlias(alias)); + int count = 1; + bool countSent = false; + string countString; + if (fields.TryGetValue("count." + alias, out countString)) { + if (!int.TryParse(countString, out count) || count < 0) { + Logger.OpenId.ErrorFormat("Failed to parse count.{0} value to a non-negative integer.", alias); + continue; + } + countSent = true; + } + if (countSent) { + for (int i = 1; i <= count; i++) { + string value; + if (fields.TryGetValue(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i), out value)) { + att.Values.Add(value); + } else { + Logger.OpenId.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri); + continue; + } + } + } else { + string value; + if (fields.TryGetValue("value." + alias, out value)) { + att.Values.Add(value); + } else { + Logger.OpenId.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri); + continue; + } + } + yield return att; + } + } + + /// <summary> + /// Reads through the attributes included in the response to discover + /// the alias-TypeURI relationships. + /// </summary> + /// <param name="fields">The data included in the extension message.</param> + /// <returns>The alias manager that provides lookup between aliases and type URIs.</returns> + private static AliasManager ParseAliases(IDictionary<string, string> fields) { + Contract.Requires<ArgumentNullException>(fields != null); + + AliasManager aliasManager = new AliasManager(); + const string TypePrefix = "type."; + foreach (var pair in fields) { + if (!pair.Key.StartsWith(TypePrefix, StringComparison.Ordinal)) { + continue; + } + string alias = pair.Key.Substring(TypePrefix.Length); + if (alias.IndexOfAny(FetchRequest.IllegalAliasCharacters) >= 0) { + Logger.OpenId.ErrorFormat("Illegal characters in alias name '{0}'.", alias); + continue; + } + aliasManager.SetAlias(alias, pair.Value); + } + return aliasManager; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs new file mode 100644 index 0000000..2dc9c69 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeRequest.cs @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------- +// <copyright file="AttributeRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An individual attribute to be requested of the OpenID Provider using + /// the Attribute Exchange extension. + /// </summary> + [Serializable] + [DebuggerDisplay("{TypeUri} (required: {IsRequired}) ({Count})")] + public class AttributeRequest { + /// <summary> + /// Backing field for the <see cref="Count"/> property. + /// </summary> + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private int count = 1; + + /// <summary> + /// Initializes a new instance of the <see cref="AttributeRequest"/> class + /// with <see cref="IsRequired"/> = false, <see cref="Count"/> = 1. + /// </summary> + public AttributeRequest() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AttributeRequest"/> class + /// with <see cref="IsRequired"/> = false, <see cref="Count"/> = 1. + /// </summary> + /// <param name="typeUri">The unique TypeURI for that describes the attribute being sought.</param> + public AttributeRequest(string typeUri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + this.TypeUri = typeUri; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AttributeRequest"/> class + /// with <see cref="Count"/> = 1. + /// </summary> + /// <param name="typeUri">The unique TypeURI for that describes the attribute being sought.</param> + /// <param name="isRequired">A value indicating whether the Relying Party considers this attribute to be required for registration.</param> + public AttributeRequest(string typeUri, bool isRequired) + : this(typeUri) { + this.IsRequired = isRequired; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AttributeRequest"/> class. + /// </summary> + /// <param name="typeUri">The unique TypeURI for that describes the attribute being sought.</param> + /// <param name="isRequired">A value indicating whether the Relying Party considers this attribute to be required for registration.</param> + /// <param name="count">The maximum number of values for this attribute the Relying Party is prepared to receive.</param> + public AttributeRequest(string typeUri, bool isRequired, int count) + : this(typeUri, isRequired) { + this.Count = count; + } + + /// <summary> + /// Gets or sets the URI uniquely identifying the attribute being requested. + /// </summary> + public string TypeUri { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the relying party considers this a required field. + /// Note that even if set to true, the Provider may not provide the value. + /// </summary> + public bool IsRequired { get; set; } + + /// <summary> + /// Gets or sets the maximum number of values for this attribute the + /// Relying Party wishes to receive from the OpenID Provider. + /// A value of int.MaxValue is considered infinity. + /// </summary> + public int Count { + get { + return this.count; + } + + set { + Contract.Requires<ArgumentOutOfRangeException>(value > 0); + this.count = value; + } + } + + /// <summary> + /// Used by a Provider to create a response to a request for an attribute's value(s) + /// using a given array of strings. + /// </summary> + /// <param name="values">The values for the requested attribute.</param> + /// <returns> + /// The newly created <see cref="AttributeValues"/> object that should be added to + /// the <see cref="FetchResponse"/> object. + /// </returns> + public AttributeValues Respond(params string[] values) { + Contract.Requires<ArgumentNullException>(values != null); + Contract.Requires<ArgumentException>(values.Length <= this.Count); + return new AttributeValues(this.TypeUri, values); + } + + /// <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) { + AttributeRequest other = obj as AttributeRequest; + if (other == null) { + return false; + } + + if (this.TypeUri != other.TypeUri) { + return false; + } + + if (this.Count != other.Count) { + return false; + } + + if (this.IsRequired != other.IsRequired) { + return false; + } + + return true; + } + + /// <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() { + int hashCode = this.IsRequired ? 1 : 0; + unchecked { + hashCode += this.Count; + if (this.TypeUri != null) { + hashCode += this.TypeUri.GetHashCode(); + } + } + + return hashCode; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs new file mode 100644 index 0000000..b2fc1fe --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/AttributeValues.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------- +// <copyright file="AttributeValues.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An individual attribute's value(s) as supplied by an OpenID Provider + /// in response to a prior request by an OpenID Relying Party as part of + /// a fetch request, or by a relying party as part of a store request. + /// </summary> + [Serializable] + [DebuggerDisplay("{TypeUri}")] + public class AttributeValues { + /// <summary> + /// Initializes a new instance of the <see cref="AttributeValues"/> class. + /// </summary> + /// <param name="typeUri">The TypeURI that uniquely identifies the attribute.</param> + /// <param name="values">The values for the attribute.</param> + public AttributeValues(string typeUri, params string[] values) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + + this.TypeUri = typeUri; + this.Values = (IList<string>)values ?? EmptyList<string>.Instance; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AttributeValues"/> class. + /// </summary> + /// <remarks> + /// This is internal because web sites should be using the + /// <see cref="AttributeRequest.Respond"/> method to instantiate. + /// </remarks> + internal AttributeValues() { + this.Values = new List<string>(1); + } + + /// <summary> + /// Initializes a new instance of the <see cref="AttributeValues"/> class. + /// </summary> + /// <param name="typeUri">The TypeURI of the attribute whose values are being provided.</param> + internal AttributeValues(string typeUri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + + this.TypeUri = typeUri; + this.Values = new List<string>(1); + } + + /// <summary> + /// Gets the URI uniquely identifying the attribute whose value is being supplied. + /// </summary> + public string TypeUri { get; internal set; } + + /// <summary> + /// Gets the values supplied by the Provider. + /// </summary> + public IList<string> Values { get; private set; } + + /// <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) { + AttributeValues other = obj as AttributeValues; + if (other == null) { + return false; + } + + if (this.TypeUri != other.TypeUri) { + return false; + } + + if (!MessagingUtilities.AreEquivalent<string>(this.Values, other.Values)) { + return false; + } + + return true; + } + + /// <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() { + int hashCode = 0; + unchecked { + if (this.TypeUri != null) { + hashCode += this.TypeUri.GetHashCode(); + } + + foreach (string value in this.Values) { + hashCode += value.GetHashCode(); + } + } + + return hashCode; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs new file mode 100644 index 0000000..167d0d2 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/Constants.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="Constants.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + /// <summary> + /// Attribute Exchange constants + /// </summary> + internal static class Constants { + /// <summary> + /// The TypeURI by which the AX extension is recognized in + /// OpenID messages and in XRDS documents. + /// </summary> + internal const string TypeUri = "http://openid.net/srv/ax/1.0"; + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs new file mode 100644 index 0000000..124a18c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchRequest.cs @@ -0,0 +1,285 @@ +//----------------------------------------------------------------------- +// <copyright file="FetchRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The Attribute Exchange Fetch message, request leg. + /// </summary> + [Serializable] + public sealed class FetchRequest : ExtensionBase, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && isProviderRole) { + string mode; + if (data.TryGetValue("mode", out mode) && mode == Mode) { + return new FetchRequest(); + } + } + + return null; + }; + + /// <summary> + /// Characters that may not appear in an attribute alias list. + /// </summary> + internal static readonly char[] IllegalAliasListCharacters = new[] { '.', '\n' }; + + /// <summary> + /// Characters that may not appear in an attribute Type URI alias. + /// </summary> + internal static readonly char[] IllegalAliasCharacters = new[] { '.', ',', ':' }; + + /// <summary> + /// The value for the 'mode' parameter. + /// </summary> + [MessagePart("mode", IsRequired = true)] + private const string Mode = "fetch_request"; + + /// <summary> + /// The collection of requested attributes. + /// </summary> + private readonly KeyedCollection<string, AttributeRequest> attributes = new KeyedCollectionDelegate<string, AttributeRequest>(ar => ar.TypeUri); + + /// <summary> + /// Initializes a new instance of the <see cref="FetchRequest"/> class. + /// </summary> + public FetchRequest() + : base(new Version(1, 0), Constants.TypeUri, null) { + } + + /// <summary> + /// Gets a collection of the attributes whose values are + /// requested by the Relying Party. + /// </summary> + /// <value>A collection where the keys are the attribute type URIs, and the value + /// is all the attribute request details.</value> + public KeyedCollection<string, AttributeRequest> Attributes { + get { + Contract.Ensures(Contract.Result<KeyedCollection<string, AttributeRequest>>() != null); + return this.attributes; + } + } + + /// <summary> + /// Gets or sets the URL that the OpenID Provider may re-post the fetch response + /// message to at some time after the initial response has been sent, using an + /// OpenID Authentication Positive Assertion to inform the relying party of updates + /// to the requested fields. + /// </summary> + [MessagePart("update_url", IsRequired = false)] + public Uri UpdateUrl { get; set; } + + /// <summary> + /// Gets or sets a list of aliases for optional attributes. + /// </summary> + /// <value>A comma-delimited list of aliases.</value> + [MessagePart("if_available", IsRequired = false)] + private string OptionalAliases { get; set; } + + /// <summary> + /// Gets or sets a list of aliases for required attributes. + /// </summary> + /// <value>A comma-delimited list of aliases.</value> + [MessagePart("required", IsRequired = false)] + private string RequiredAliases { get; set; } + + /// <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) { + FetchRequest other = obj as FetchRequest; + if (other == null) { + return false; + } + + if (this.Version != other.Version) { + return false; + } + + if (this.UpdateUrl != other.UpdateUrl) { + return false; + } + + if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) { + return false; + } + + return true; + } + + /// <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() { + unchecked { + int hashCode = this.Version.GetHashCode(); + + if (this.UpdateUrl != null) { + hashCode += this.UpdateUrl.GetHashCode(); + } + + foreach (AttributeRequest att in this.Attributes) { + hashCode += att.GetHashCode(); + } + + return hashCode; + } + } + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + var fields = ((IMessage)this).ExtraData; + fields.Clear(); + + List<string> requiredAliases = new List<string>(), optionalAliases = new List<string>(); + AliasManager aliasManager = new AliasManager(); + foreach (var att in this.attributes) { + string alias = aliasManager.GetAlias(att.TypeUri); + + // define the alias<->typeUri mapping + fields.Add("type." + alias, att.TypeUri); + + // set how many values the relying party wants max + fields.Add("count." + alias, att.Count.ToString(CultureInfo.InvariantCulture)); + + if (att.IsRequired) { + requiredAliases.Add(alias); + } else { + optionalAliases.Add(alias); + } + } + + // Set optional/required lists + this.OptionalAliases = optionalAliases.Count > 0 ? string.Join(",", optionalAliases.ToArray()) : null; + this.RequiredAliases = requiredAliases.Count > 0 ? string.Join(",", requiredAliases.ToArray()) : null; + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + var extraData = ((IMessage)this).ExtraData; + var requiredAliases = ParseAliasList(this.RequiredAliases); + var optionalAliases = ParseAliasList(this.OptionalAliases); + + // if an alias shows up in both lists, an exception will result implicitly. + var allAliases = new List<string>(requiredAliases.Count + optionalAliases.Count); + allAliases.AddRange(requiredAliases); + allAliases.AddRange(optionalAliases); + if (allAliases.Count == 0) { + Logger.OpenId.Error("Attribute Exchange extension did not provide any aliases in the if_available or required lists."); + return; + } + + AliasManager aliasManager = new AliasManager(); + foreach (var alias in allAliases) { + string attributeTypeUri; + if (extraData.TryGetValue("type." + alias, out attributeTypeUri)) { + aliasManager.SetAlias(alias, attributeTypeUri); + AttributeRequest att = new AttributeRequest { + TypeUri = attributeTypeUri, + IsRequired = requiredAliases.Contains(alias), + }; + string countString; + if (extraData.TryGetValue("count." + alias, out countString)) { + if (countString == "unlimited") { + att.Count = int.MaxValue; + } else { + int count; + if (int.TryParse(countString, out count) && count > 0) { + att.Count = count; + } else { + Logger.OpenId.Error("count." + alias + " could not be parsed into a positive integer."); + } + } + } else { + att.Count = 1; + } + this.Attributes.Add(att); + } else { + Logger.OpenId.Error("Type URI definition of alias " + alias + " is missing."); + } + } + } + + #endregion + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.UpdateUrl != null && !this.UpdateUrl.IsAbsoluteUri) { + this.UpdateUrl = null; + Logger.OpenId.ErrorFormat("The AX fetch request update_url parameter was not absolute ('{0}'). Ignoring value.", this.UpdateUrl); + } + + if (this.OptionalAliases != null) { + if (this.OptionalAliases.IndexOfAny(IllegalAliasListCharacters) >= 0) { + Logger.OpenId.Error("Illegal characters found in Attribute Exchange if_available alias list. Ignoring value."); + this.OptionalAliases = null; + } + } + + if (this.RequiredAliases != null) { + if (this.RequiredAliases.IndexOfAny(IllegalAliasListCharacters) >= 0) { + Logger.OpenId.Error("Illegal characters found in Attribute Exchange required alias list. Ignoring value."); + this.RequiredAliases = null; + } + } + } + + /// <summary> + /// Splits a list of aliases by their commas. + /// </summary> + /// <param name="aliasList">The comma-delimited list of aliases. May be null or empty.</param> + /// <returns>The list of aliases. Never null, but may be empty.</returns> + private static IList<string> ParseAliasList(string aliasList) { + if (string.IsNullOrEmpty(aliasList)) { + return EmptyList<string>.Instance; + } + + return aliasList.Split(','); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs new file mode 100644 index 0000000..14b1caa --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/FetchResponse.cs @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------- +// <copyright file="FetchResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using System.Collections.ObjectModel; + using System.Diagnostics.Contracts; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The Attribute Exchange Fetch message, response leg. + /// </summary> + [Serializable] + public sealed class FetchResponse : ExtensionBase, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && !isProviderRole) { + string mode; + if (data.TryGetValue("mode", out mode) && mode == Mode) { + return new FetchResponse(); + } + } + + return null; + }; + + /// <summary> + /// The value of the 'mode' parameter. + /// </summary> + [MessagePart("mode", IsRequired = true)] + private const string Mode = "fetch_response"; + + /// <summary> + /// The collection of provided attributes. This field will never be null. + /// </summary> + private readonly KeyedCollection<string, AttributeValues> attributesProvided = new KeyedCollectionDelegate<string, AttributeValues>(av => av.TypeUri); + + /// <summary> + /// Initializes a new instance of the <see cref="FetchResponse"/> class. + /// </summary> + public FetchResponse() + : base(new Version(1, 0), Constants.TypeUri, null) { + } + + /// <summary> + /// Gets a sequence of the attributes whose values are provided by the OpenID Provider. + /// </summary> + public KeyedCollection<string, AttributeValues> Attributes { + get { + Contract.Ensures(Contract.Result<KeyedCollection<string, AttributeValues>>() != null); + return this.attributesProvided; + } + } + + /// <summary> + /// Gets a value indicating whether the OpenID Provider intends to + /// honor the request for updates. + /// </summary> + public bool UpdateUrlSupported { + get { return this.UpdateUrl != null; } + } + + /// <summary> + /// Gets or sets the URL the OpenID Provider will post updates to. + /// Must be set if the Provider supports and will use this feature. + /// </summary> + [MessagePart("update_url", IsRequired = false)] + public Uri UpdateUrl { get; set; } + + /// <summary> + /// Gets a value indicating whether this extension is signed by the Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>. + /// </value> + public bool IsSignedByProvider { + get { return this.IsSignedByRemoteParty; } + } + + /// <summary> + /// Gets the first attribute value provided for a given attribute Type URI. + /// </summary> + /// <param name="typeUri"> + /// The type URI of the attribute. + /// Usually a constant from <see cref="WellKnownAttributes"/>.</param> + /// <returns> + /// The first value provided for the attribute, or <c>null</c> if the attribute is missing or no values were provided. + /// </returns> + /// <remarks> + /// This is meant as a helper method for the common case of just wanting one attribute value. + /// For greater flexibility or to retrieve more than just the first value for an attribute, + /// use the <see cref="Attributes"/> collection directly. + /// </remarks> + public string GetAttributeValue(string typeUri) { + if (this.Attributes.Contains(typeUri)) { + return this.Attributes[typeUri].Values.FirstOrDefault(); + } else { + return null; + } + } + + /// <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) { + FetchResponse other = obj as FetchResponse; + if (other == null) { + return false; + } + + if (this.Version != other.Version) { + return false; + } + + if (this.UpdateUrl != other.UpdateUrl) { + return false; + } + + if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) { + return false; + } + + return true; + } + + /// <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() { + unchecked { + int hashCode = this.Version.GetHashCode(); + + if (this.UpdateUrl != null) { + hashCode += this.UpdateUrl.GetHashCode(); + } + + foreach (AttributeValues value in this.Attributes) { + hashCode += value.GetHashCode(); + } + + return hashCode; + } + } + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + var extraData = ((IMessage)this).ExtraData; + AXUtilities.SerializeAttributes(extraData, this.attributesProvided); + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + var extraData = ((IMessage)this).ExtraData; + foreach (var att in AXUtilities.DeserializeAttributes(extraData)) { + this.Attributes.Add(att); + } + } + + #endregion + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.UpdateUrl != null && !this.UpdateUrl.IsAbsoluteUri) { + this.UpdateUrl = null; + Logger.OpenId.ErrorFormat("The AX fetch response update_url parameter was not absolute ('{0}'). Ignoring value.", this.UpdateUrl); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs new file mode 100644 index 0000000..641b17a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreRequest.cs @@ -0,0 +1,127 @@ +//----------------------------------------------------------------------- +// <copyright file="StoreRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using System.Collections.ObjectModel; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The Attribute Exchange Store message, request leg. + /// </summary> + [Serializable] + public sealed class StoreRequest : ExtensionBase, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && isProviderRole) { + string mode; + if (data.TryGetValue("mode", out mode) && mode == Mode) { + return new StoreRequest(); + } + } + + return null; + }; + + /// <summary> + /// The value of the 'mode' parameter. + /// </summary> + [MessagePart("mode", IsRequired = true)] + private const string Mode = "store_request"; + + /// <summary> + /// The collection of provided attribute values. This field will never be null. + /// </summary> + private readonly KeyedCollection<string, AttributeValues> attributesProvided = new KeyedCollectionDelegate<string, AttributeValues>(av => av.TypeUri); + + /// <summary> + /// Initializes a new instance of the <see cref="StoreRequest"/> class. + /// </summary> + public StoreRequest() + : base(new Version(1, 0), Constants.TypeUri, null) { + } + + /// <summary> + /// Gets the collection of all the attributes that are included in the store request. + /// </summary> + public KeyedCollection<string, AttributeValues> Attributes { + get { return this.attributesProvided; } + } + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + var fields = ((IMessage)this).ExtraData; + fields.Clear(); + + AXUtilities.SerializeAttributes(fields, this.attributesProvided); + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + var fields = ((IMessage)this).ExtraData; + foreach (var att in AXUtilities.DeserializeAttributes(fields)) { + this.Attributes.Add(att); + } + } + + #endregion + + /// <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 StoreRequest; + if (other == null) { + return false; + } + + if (this.Version != other.Version) { + return false; + } + + if (!MessagingUtilities.AreEquivalentUnordered(this.Attributes.ToList(), other.Attributes.ToList())) { + return false; + } + + return true; + } + + /// <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() { + unchecked { + int hashCode = this.Version.GetHashCode(); + foreach (AttributeValues att in this.Attributes) { + hashCode += att.GetHashCode(); + } + return hashCode; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs new file mode 100644 index 0000000..ba7f091 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/StoreResponse.cs @@ -0,0 +1,164 @@ +//----------------------------------------------------------------------- +// <copyright file="StoreResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The Attribute Exchange Store message, response leg. + /// </summary> + [Serializable] + public sealed class StoreResponse : ExtensionBase { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && !isProviderRole) { + string mode; + if (data.TryGetValue("mode", out mode) && (mode == SuccessMode || mode == FailureMode)) { + return new StoreResponse(); + } + } + + return null; + }; + + /// <summary> + /// The value of the mode parameter used to express a successful store operation. + /// </summary> + private const string SuccessMode = "store_response_success"; + + /// <summary> + /// The value of the mode parameter used to express a store operation failure. + /// </summary> + private const string FailureMode = "store_response_failure"; + + /// <summary> + /// Initializes a new instance of the <see cref="StoreResponse"/> class + /// to represent a successful store operation. + /// </summary> + public StoreResponse() + : base(new Version(1, 0), Constants.TypeUri, null) { + this.Succeeded = true; + } + + /// <summary> + /// Initializes a new instance of the <see cref="StoreResponse"/> class + /// to represent a failed store operation. + /// </summary> + /// <param name="failureReason">The reason for failure.</param> + public StoreResponse(string failureReason) + : this() { + this.Succeeded = false; + this.FailureReason = failureReason; + } + + /// <summary> + /// Gets or sets a value indicating whether the storage request succeeded. + /// </summary> + /// <value>Defaults to <c>true</c>.</value> + public bool Succeeded { + get { return this.Mode == SuccessMode; } + set { this.Mode = value ? SuccessMode : FailureMode; } + } + + /// <summary> + /// Gets or sets the reason for the failure, if applicable. + /// </summary> + [MessagePart("error", IsRequired = false)] + public string FailureReason { get; set; } + + /// <summary> + /// Gets a value indicating whether this extension is signed by the Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>. + /// </value> + public bool IsSignedByProvider { + get { return this.IsSignedByRemoteParty; } + } + + /// <summary> + /// Gets or sets the mode argument. + /// </summary> + /// <value>One of 'store_response_success' or 'store_response_failure'.</value> + [MessagePart("mode", IsRequired = true)] + private string Mode { get; set; } + + /// <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 StoreResponse; + if (other == null) { + return false; + } + + if (this.Version != other.Version) { + return false; + } + + if (this.Succeeded != other.Succeeded) { + return false; + } + + if (this.FailureReason != other.FailureReason) { + return false; + } + + return true; + } + + /// <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() { + unchecked { + int hashCode = this.Version.GetHashCode(); + hashCode += this.Succeeded ? 0 : 1; + if (this.FailureReason != null) { + hashCode += this.FailureReason.GetHashCode(); + } + + return hashCode; + } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + ErrorUtilities.VerifyProtocol( + this.Mode == SuccessMode || this.Mode == FailureMode, + MessagingStrings.UnexpectedMessagePartValue, + "mode", + this.Mode); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs new file mode 100644 index 0000000..5aa89c6 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/AttributeExchange/WellKnownAttributes.cs @@ -0,0 +1,323 @@ +//----------------------------------------------------------------------- +// <copyright file="WellKnownAttributes.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.AttributeExchange { + using System.Diagnostics.CodeAnalysis; + + /// <summary> + /// Attribute types defined at http://www.axschema.org/types/. + /// </summary> + /// <remarks> + /// If you don't see what you need here, check that URL to see if any have been added. + /// You can use new ones directly without adding them to this class, and can even make + /// up your own if you expect the other end to understand what you make up. + /// </remarks> + public static class WellKnownAttributes { + /// <summary> + /// Inherent attributes about a personality such as gender and bio. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Person { + /// <summary>Gender, either "M" or "F"</summary> + /// <example>"M", "F"</example> + public const string Gender = "http://axschema.org/person/gender"; + + /// <summary>Biography (text)</summary> + /// <example>"I am the very model of a modern Major General."</example> + public const string Biography = "http://axschema.org/media/biography"; + } + + /// <summary> + /// Preferences such as language and timezone. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Preferences { + /// <summary>Preferred language, as per RFC4646</summary> + /// <example>"en-US"</example> + public const string Language = "http://axschema.org/pref/language"; + + /// <summary>Home time zone information (as specified in <a href="http://en.wikipedia.org/wiki/List_of_tz_zones_by_name">zoneinfo</a>)</summary> + /// <example>"America/Pacific"</example> + public const string TimeZone = "http://axschema.org/pref/timezone"; + } + + /// <summary> + /// The names a person goes by. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Name { + /// <summary>Subject's alias or "screen" name</summary> + /// <example>"Johnny5"</example> + public const string Alias = "http://axschema.org/namePerson/friendly"; + + /// <summary>Full name of subject</summary> + /// <example>"John Doe"</example> + public const string FullName = "http://axschema.org/namePerson"; + + /// <summary>Honorific prefix for the subject's name</summary> + /// <example>"Mr.", "Mrs.", "Dr."</example> + public const string Prefix = "http://axschema.org/namePerson/prefix"; + + /// <summary>First or given name of subject</summary> + /// <example>"John"</example> + public const string First = "http://axschema.org/namePerson/first"; + + /// <summary>Last name or surname of subject</summary> + /// <example>"Smith"</example> + public const string Last = "http://axschema.org/namePerson/last"; + + /// <summary>Middle name(s) of subject</summary> + /// <example>"Robert"</example> + public const string Middle = "http://axschema.org/namePerson/middle"; + + /// <summary>Suffix of subject's name</summary> + /// <example>"III", "Jr."</example> + public const string Suffix = "http://axschema.org/namePerson/suffix"; + } + + /// <summary> + /// Business affiliation. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Company { + /// <summary>Company name (employer)</summary> + /// <example>"Springfield Power"</example> + public const string CompanyName = "http://axschema.org/company/name"; + + /// <summary>Employee title</summary> + /// <example>"Engineer"</example> + public const string JobTitle = "http://axschema.org/company/title"; + } + + /// <summary> + /// Information about a person's birthdate. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class BirthDate { + /// <summary>Date of birth.</summary> + /// <example>"1979-01-01"</example> + public const string WholeBirthDate = "http://axschema.org/birthDate"; + + /// <summary>Year of birth (four digits)</summary> + /// <example>"1979"</example> + public const string Year = "http://axschema.org/birthDate/birthYear"; + + /// <summary>Month of birth (1-12)</summary> + /// <example>"05"</example> + public const string Month = "http://axschema.org/birthDate/birthMonth"; + + /// <summary>Day of birth</summary> + /// <example>"31"</example> + public const string DayOfMonth = "http://axschema.org/birthDate/birthday"; + } + + /// <summary> + /// Various ways to contact a person. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Contact { + /// <summary>Internet SMTP email address as per RFC2822</summary> + /// <example>"jsmith@isp.example.com"</example> + public const string Email = "http://axschema.org/contact/email"; + + /// <summary> + /// Various types of phone numbers. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Phone { + /// <summary>Main phone number (preferred)</summary> + /// <example>+1-800-555-1234</example> + public const string Preferred = "http://axschema.org/contact/phone/default"; + + /// <summary>Home phone number</summary> + /// <example>+1-800-555-1234</example> + public const string Home = "http://axschema.org/contact/phone/home"; + + /// <summary>Business phone number</summary> + /// <example>+1-800-555-1234</example> + public const string Work = "http://axschema.org/contact/phone/business"; + + /// <summary>Cellular (or mobile) phone number</summary> + /// <example>+1-800-555-1234</example> + public const string Mobile = "http://axschema.org/contact/phone/cell"; + + /// <summary>Fax number</summary> + /// <example>+1-800-555-1234</example> + public const string Fax = "http://axschema.org/contact/phone/fax"; + } + + /// <summary> + /// The many fields that make up an address. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class HomeAddress { + /// <summary>Home postal address: street number, name and apartment number</summary> + /// <example>"#42 135 East 1st Street"</example> + public const string StreetAddressLine1 = "http://axschema.org/contact/postalAddress/home"; + + /// <summary>"#42 135 East 1st Street"</summary> + /// <example>"Box 67"</example> + public const string StreetAddressLine2 = "http://axschema.org/contact/postalAddressAdditional/home"; + + /// <summary>Home city name</summary> + /// <example>"Vancouver"</example> + public const string City = "http://axschema.org/contact/city/home"; + + /// <summary>Home state or province name</summary> + /// <example>"BC"</example> + public const string State = "http://axschema.org/contact/state/home"; + + /// <summary>Home country code in ISO.3166.1988 (alpha 2) format</summary> + /// <example>"CA"</example> + public const string Country = "http://axschema.org/contact/country/home"; + + /// <summary>Home postal code; region specific format</summary> + /// <example>"V5A 4B2"</example> + public const string PostalCode = "http://axschema.org/contact/postalCode/home"; + } + + /// <summary> + /// The many fields that make up an address. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class WorkAddress { + /// <summary>Business postal address: street number, name and apartment number</summary> + /// <example>"#42 135 East 1st Street"</example> + public const string StreetAddressLine1 = "http://axschema.org/contact/postalAddress/business"; + + /// <summary>"#42 135 East 1st Street"</summary> + /// <example>"Box 67"</example> + public const string StreetAddressLine2 = "http://axschema.org/contact/postalAddressAdditional/business"; + + /// <summary>Business city name</summary> + /// <example>"Vancouver"</example> + public const string City = "http://axschema.org/contact/city/business"; + + /// <summary>Business state or province name</summary> + /// <example>"BC"</example> + public const string State = "http://axschema.org/contact/state/business"; + + /// <summary>Business country code in ISO.3166.1988 (alpha 2) format</summary> + /// <example>"CA"</example> + public const string Country = "http://axschema.org/contact/country/business"; + + /// <summary>Business postal code; region specific format</summary> + /// <example>"V5A 4B2"</example> + public const string PostalCode = "http://axschema.org/contact/postalCode/business"; + } + + /// <summary> + /// Various handles for instant message clients. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class IM { + /// <summary>AOL instant messaging service handle</summary> + /// <example>"jsmith421234"</example> + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "AOL", Justification = "By design")] + public const string AOL = "http://axschema.org/contact/IM/AIM"; + + /// <summary>ICQ instant messaging service handle</summary> + /// <example>"1234567"</example> + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ICQ", Justification = "By design")] + public const string ICQ = "http://axschema.org/contact/IM/ICQ"; + + /// <summary>MSN instant messaging service handle</summary> + /// <example>"jsmith42@hotmail.com"</example> + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "MSN", Justification = "By design")] + public const string MSN = "http://axschema.org/contact/IM/MSN"; + + /// <summary>Yahoo! instant messaging service handle</summary> + /// <example>"jsmith421234"</example> + public const string Yahoo = "http://axschema.org/contact/IM/Yahoo"; + + /// <summary>Jabber instant messaging service handle</summary> + /// <example>"jsmith@jabber.example.com"</example> + public const string Jabber = "http://axschema.org/contact/IM/Jabber"; + + /// <summary>Skype instant messaging service handle</summary> + /// <example>"jsmith42"</example> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Skype", Justification = "By design")] + public const string Skype = "http://axschema.org/contact/IM/Skype"; + } + + /// <summary> + /// Various web addresses connected with this personality. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "By design"), SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Web { + /// <summary>Web site URL</summary> + /// <example>"http://example.com/~jsmith/"</example> + public const string Homepage = "http://axschema.org/contact/web/default"; + + /// <summary>Blog home page URL</summary> + /// <example>"http://example.com/jsmith_blog/"</example> + public const string Blog = "http://axschema.org/contact/web/blog"; + + /// <summary>LinkedIn URL</summary> + /// <example>"http://www.linkedin.com/pub/1/234/56"</example> + public const string LinkedIn = "http://axschema.org/contact/web/Linkedin"; + + /// <summary>Amazon URL</summary> + /// <example>"http://www.amazon.com/gp/pdp/profile/A24DLKJ825"</example> + public const string Amazon = "http://axschema.org/contact/web/Amazon"; + + /// <summary>Flickr URL</summary> + /// <example>"http://flickr.com/photos/jsmith42/"</example> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Flickr", Justification = "By design")] + public const string Flickr = "http://axschema.org/contact/web/Flickr"; + + /// <summary>del.icio.us URL</summary> + /// <example>"http://del.icio.us/jsmith42"</example> + public const string Delicious = "http://axschema.org/contact/web/Delicious"; + } + } + + /// <summary> + /// Audio and images of this personality. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "By design"), SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Media { + /// <summary>Spoken name (web URL)</summary> + /// <example>"http://example.com/~jsmith/john_smith.wav"</example> + public const string SpokenName = "http://axschema.org/media/spokenname"; + + /// <summary>Audio greeting (web URL)</summary> + /// <example>"http://example.com/~jsmith/i_greet_you.wav"</example> + public const string AudioGreeting = "http://axschema.org/media/greeting/audio"; + + /// <summary>Video greeting (web URL)</summary> + /// <example>"http://example.com/~jsmith/i_greet_you.mov"</example> + public const string VideoGreeting = "http://axschema.org/media/greeting/video"; + + /// <summary> + /// Images of this personality. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Required for desired autocompletion.")] + public static class Images { + /// <summary>Image (web URL); unspecified dimension</summary> + /// <example>"http://example.com/~jsmith/image.jpg"</example> + public const string Default = "http://axschema.org/media/image/default"; + + /// <summary>Image (web URL) with equal width and height</summary> + /// <example>"http://example.com/~jsmith/image.jpg"</example> + public const string Aspect11 = "http://axschema.org/media/image/aspect11"; + + /// <summary>Image (web URL) 4:3 aspect ratio - landscape</summary> + /// <example>"http://example.com/~jsmith/image.jpg"</example> + public const string Aspect43 = "http://axschema.org/media/image/aspect43"; + + /// <summary>Image (web URL) 4:3 aspect ratio - landscape</summary> + /// <example>"http://example.com/~jsmith/image.jpg"</example> + public const string Aspect34 = "http://axschema.org/media/image/aspect34"; + + /// <summary>Image (web URL); favicon format as per FAVICON-W3C. The format for the image must be 16x16 pixels or 32x32 pixels, using either 8-bit or 24-bit colors. The format of the image must be one of PNG (a W3C standard), GIF, or ICO.</summary> + /// <example>"http://example.com/~jsmith/image.jpg"</example> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Fav", Justification = "By design")] + public const string FavIcon = "http://axschema.org/media/image/favicon"; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs new file mode 100644 index 0000000..0a78df1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs @@ -0,0 +1,223 @@ +//----------------------------------------------------------------------- +// <copyright file="ExtensionArgumentsManager.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.Contracts; + using System.Globalization; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Manages the processing and construction of OpenID extensions parts. + /// </summary> + internal class ExtensionArgumentsManager { + /// <summary> + /// This contains a set of aliases that we must be willing to implicitly + /// match to namespaces for backward compatibility with other OpenID libraries. + /// </summary> + private static readonly Dictionary<string, string> typeUriToAliasAffinity = new Dictionary<string, string> { + { Extensions.SimpleRegistration.Constants.sreg_ns, Extensions.SimpleRegistration.Constants.sreg_compatibility_alias }, + { Extensions.ProviderAuthenticationPolicy.Constants.TypeUri, Extensions.ProviderAuthenticationPolicy.Constants.CompatibilityAlias }, + }; + + /// <summary> + /// The version of OpenID that the message is using. + /// </summary> + private Protocol protocol; + + /// <summary> + /// Whether extensions are being read or written. + /// </summary> + private bool isReadMode; + + /// <summary> + /// The alias manager that will track Type URI to alias mappings. + /// </summary> + private AliasManager aliasManager = new AliasManager(); + + /// <summary> + /// A complex dictionary where the key is the Type URI of the extension, + /// and the value is another dictionary of the name/value args of the extension. + /// </summary> + private Dictionary<string, IDictionary<string, string>> extensions = new Dictionary<string, IDictionary<string, string>>(); + + /// <summary> + /// Prevents a default instance of the <see cref="ExtensionArgumentsManager"/> class from being created. + /// </summary> + private ExtensionArgumentsManager() { } + + /// <summary> + /// Gets a value indicating whether the extensions are being read (as opposed to written). + /// </summary> + internal bool ReadMode { + get { return this.isReadMode; } + } + + /// <summary> + /// Creates a <see cref="ExtensionArgumentsManager"/> instance to process incoming extensions. + /// </summary> + /// <param name="query">The parameters in the OpenID message.</param> + /// <returns>The newly created instance of <see cref="ExtensionArgumentsManager"/>.</returns> + public static ExtensionArgumentsManager CreateIncomingExtensions(IDictionary<string, string> query) { + Contract.Requires<ArgumentNullException>(query != null); + var mgr = new ExtensionArgumentsManager(); + mgr.protocol = Protocol.Detect(query); + mgr.isReadMode = true; + string aliasPrefix = mgr.protocol.openid.ns + "."; + + // First pass looks for namespace aliases + foreach (var pair in query) { + if (pair.Key.StartsWith(aliasPrefix, StringComparison.Ordinal)) { + mgr.aliasManager.SetAlias(pair.Key.Substring(aliasPrefix.Length), pair.Value); + } + } + + // For backwards compatibility, add certain aliases if they aren't defined. + if (mgr.protocol.Version.Major < 2) { + foreach (var pair in typeUriToAliasAffinity) { + if (!mgr.aliasManager.IsAliasAssignedTo(pair.Key) && + !mgr.aliasManager.IsAliasUsed(pair.Value)) { + mgr.aliasManager.SetAlias(pair.Value, pair.Key); + } + } + } + + // Second pass looks for extensions using those aliases + foreach (var pair in query) { + if (!pair.Key.StartsWith(mgr.protocol.openid.Prefix, StringComparison.Ordinal)) { + continue; + } + string possibleAlias = pair.Key.Substring(mgr.protocol.openid.Prefix.Length); + int periodIndex = possibleAlias.IndexOf(".", StringComparison.Ordinal); + if (periodIndex >= 0) { + possibleAlias = possibleAlias.Substring(0, periodIndex); + } + string typeUri; + if ((typeUri = mgr.aliasManager.TryResolveAlias(possibleAlias)) != null) { + if (!mgr.extensions.ContainsKey(typeUri)) { + mgr.extensions[typeUri] = new Dictionary<string, string>(); + } + string key = periodIndex >= 0 ? pair.Key.Substring(mgr.protocol.openid.Prefix.Length + possibleAlias.Length + 1) : string.Empty; + mgr.extensions[typeUri].Add(key, pair.Value); + } + } + return mgr; + } + + /// <summary> + /// Creates a <see cref="ExtensionArgumentsManager"/> instance to prepare outgoing extensions. + /// </summary> + /// <param name="protocol">The protocol version used for the outgoing message.</param> + /// <returns> + /// The newly created instance of <see cref="ExtensionArgumentsManager"/>. + /// </returns> + public static ExtensionArgumentsManager CreateOutgoingExtensions(Protocol protocol) { + var mgr = new ExtensionArgumentsManager(); + mgr.protocol = protocol; + + // Affinity for certain alias for backwards compatibility + foreach (var pair in typeUriToAliasAffinity) { + mgr.aliasManager.SetAlias(pair.Value, pair.Key); + } + return mgr; + } + + /// <summary> + /// Adds query parameters for OpenID extensions to the request directed + /// at the OpenID provider. + /// </summary> + /// <param name="extensionTypeUri">The extension type URI.</param> + /// <param name="arguments">The arguments for this extension to add to the message.</param> + public void AddExtensionArguments(string extensionTypeUri, IDictionary<string, string> arguments) { + Contract.Requires<InvalidOperationException>(!this.ReadMode); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(extensionTypeUri)); + Contract.Requires<ArgumentNullException>(arguments != null); + if (arguments.Count == 0) { + return; + } + + IDictionary<string, string> extensionArgs; + if (!this.extensions.TryGetValue(extensionTypeUri, out extensionArgs)) { + this.extensions.Add(extensionTypeUri, extensionArgs = new Dictionary<string, string>(arguments.Count)); + } + + ErrorUtilities.VerifyProtocol(extensionArgs.Count == 0, OpenIdStrings.ExtensionAlreadyAddedWithSameTypeURI, extensionTypeUri); + foreach (var pair in arguments) { + extensionArgs.Add(pair.Key, pair.Value); + } + } + + /// <summary> + /// Gets the actual arguments to add to a querystring or other response, + /// where type URI, alias, and actual key/values are all defined. + /// </summary> + /// <param name="includeOpenIdPrefix"> + /// <c>true</c> if the generated parameter names should include the 'openid.' prefix. + /// This should be <c>true</c> for all but direct response messages. + /// </param> + /// <returns>A dictionary of key=value pairs to add to the message to carry the extension.</returns> + internal IDictionary<string, string> GetArgumentsToSend(bool includeOpenIdPrefix) { + Contract.Requires<InvalidOperationException>(!this.ReadMode); + Dictionary<string, string> args = new Dictionary<string, string>(); + foreach (var typeUriAndExtension in this.extensions) { + string typeUri = typeUriAndExtension.Key; + var extensionArgs = typeUriAndExtension.Value; + if (extensionArgs.Count == 0) { + continue; + } + string alias = this.aliasManager.GetAlias(typeUri); + + // send out the alias declaration + string openidPrefix = includeOpenIdPrefix ? this.protocol.openid.Prefix : string.Empty; + args.Add(openidPrefix + this.protocol.openidnp.ns + "." + alias, typeUri); + string prefix = openidPrefix + alias; + foreach (var pair in extensionArgs) { + string key = prefix; + if (pair.Key.Length > 0) { + key += "." + pair.Key; + } + args.Add(key, pair.Value); + } + } + return args; + } + + /// <summary> + /// Gets the fields carried by a given OpenId extension. + /// </summary> + /// <param name="extensionTypeUri">The type URI of the extension whose fields are being queried for.</param> + /// <returns> + /// The fields included in the given extension, or null if the extension is not present. + /// </returns> + internal IDictionary<string, string> GetExtensionArguments(string extensionTypeUri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(extensionTypeUri)); + Contract.Requires<InvalidOperationException>(this.ReadMode); + + IDictionary<string, string> extensionArgs; + this.extensions.TryGetValue(extensionTypeUri, out extensionArgs); + return extensionArgs; + } + + /// <summary> + /// Gets whether any arguments for a given extension are present. + /// </summary> + /// <param name="extensionTypeUri">The extension Type URI in question.</param> + /// <returns><c>true</c> if this extension is present; <c>false</c> otherwise.</returns> + internal bool ContainsExtension(string extensionTypeUri) { + return this.extensions.ContainsKey(extensionTypeUri); + } + + /// <summary> + /// Gets the type URIs of all discovered extensions in the message. + /// </summary> + /// <returns>A sequence of the type URIs.</returns> + internal IEnumerable<string> GetExtensionTypeUris() { + return this.extensions.Keys; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs new file mode 100644 index 0000000..108ac52 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionBase.cs @@ -0,0 +1,190 @@ +//----------------------------------------------------------------------- +// <copyright file="ExtensionBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A handy base class for built-in extensions. + /// </summary> + [Serializable] + public class ExtensionBase : IOpenIdMessageExtension { + /// <summary> + /// Backing store for the <see cref="IOpenIdMessageExtension.TypeUri"/> property. + /// </summary> + private string typeUri; + + /// <summary> + /// Backing store for the <see cref="IOpenIdMessageExtension.AdditionalSupportedTypeUris"/> property. + /// </summary> + private IEnumerable<string> additionalSupportedTypeUris; + + /// <summary> + /// Backing store for the <see cref="IMessage.ExtraData"/> property. + /// </summary> + private Dictionary<string, string> extraData = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="ExtensionBase"/> class. + /// </summary> + /// <param name="version">The version of the extension.</param> + /// <param name="typeUri">The type URI to use in the OpenID message.</param> + /// <param name="additionalSupportedTypeUris">The additional supported type URIs by which this extension might be recognized. May be null.</param> + protected ExtensionBase(Version version, string typeUri, IEnumerable<string> additionalSupportedTypeUris) { + this.Version = version; + this.typeUri = typeUri; + this.additionalSupportedTypeUris = additionalSupportedTypeUris ?? EmptyList<string>.Instance; + } + + #region IOpenIdProtocolMessageExtension Members + + /// <summary> + /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements. + /// </summary> + string IOpenIdMessageExtension.TypeUri { + get { return this.TypeUri; } + } + + /// <summary> + /// Gets the additional TypeURIs that are supported by this extension, in preferred order. + /// May be empty if none other than <see cref="IOpenIdMessageExtension.TypeUri"/> is supported, but + /// should not be null. + /// </summary> + /// <remarks> + /// Useful for reading in messages with an older version of an extension. + /// The value in the <see cref="IOpenIdMessageExtension.TypeUri"/> property is always checked before + /// trying this list. + /// If you do support multiple versions of an extension using this method, + /// consider adding a CreateResponse method to your request extension class + /// so that the response can have the context it needs to remain compatible + /// given the version of the extension in the request message. + /// The <see cref="SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example. + /// </remarks> + IEnumerable<string> IOpenIdMessageExtension.AdditionalSupportedTypeUris { + get { return this.AdditionalSupportedTypeUris; } + } + + /// <summary> + /// Gets or sets a value indicating whether this extension was + /// signed by the OpenID Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the provider; otherwise, <c>false</c>. + /// </value> + bool IOpenIdMessageExtension.IsSignedByRemoteParty { + get { return this.IsSignedByRemoteParty; } + set { this.IsSignedByRemoteParty = value; } + } + + #endregion + + #region IMessage Properties + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + public Version Version { get; private set; } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + IDictionary<string, string> IMessage.ExtraData { + get { return this.ExtraData; } + } + + #endregion + + /// <summary> + /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements. + /// </summary> + protected string TypeUri { + get { return this.typeUri; } + } + + /// <summary> + /// Gets or sets a value indicating whether this extension was + /// signed by the OpenID Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the provider; otherwise, <c>false</c>. + /// </value> + protected bool IsSignedByRemoteParty { get; set; } + + /// <summary> + /// Gets the additional TypeURIs that are supported by this extension, in preferred order. + /// May be empty if none other than <see cref="IOpenIdMessageExtension.TypeUri"/> is supported, but + /// should not be null. + /// </summary> + /// <value></value> + /// <remarks> + /// Useful for reading in messages with an older version of an extension. + /// The value in the <see cref="IOpenIdMessageExtension.TypeUri"/> property is always checked before + /// trying this list. + /// If you do support multiple versions of an extension using this method, + /// consider adding a CreateResponse method to your request extension class + /// so that the response can have the context it needs to remain compatible + /// given the version of the extension in the request message. + /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example. + /// </remarks> + protected IEnumerable<string> AdditionalSupportedTypeUris { + get { return this.additionalSupportedTypeUris; } + } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <value></value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + protected IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + #region IMessage Methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + void IMessage.EnsureValidMessage() { + this.EnsureValidMessage(); + } + + #endregion + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + protected virtual void EnsureValidMessage() { + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionsInteropHelper.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionsInteropHelper.cs new file mode 100644 index 0000000..5e1003a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionsInteropHelper.cs @@ -0,0 +1,383 @@ +//----------------------------------------------------------------------- +// <copyright file="ExtensionsInteropHelper.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 ExtensionsInteropHelper { + /// <summary> + /// The gender decoder to translate AX genders to Sreg. + /// </summary> + private static GenderEncoder genderEncoder = new GenderEncoder(); + + /// <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 ForEachFormat(attributeFormats)) { + FetchAttribute(ax, format, WellKnownAttributes.BirthDate.WholeBirthDate, sreg.BirthDate); + FetchAttribute(ax, format, WellKnownAttributes.Contact.HomeAddress.Country, sreg.Country); + FetchAttribute(ax, format, WellKnownAttributes.Contact.Email, sreg.Email); + FetchAttribute(ax, format, WellKnownAttributes.Name.FullName, sreg.FullName); + FetchAttribute(ax, format, WellKnownAttributes.Person.Gender, sreg.Gender); + FetchAttribute(ax, format, WellKnownAttributes.Preferences.Language, sreg.Language); + FetchAttribute(ax, format, WellKnownAttributes.Name.Alias, sreg.Nickname); + FetchAttribute(ax, format, WellKnownAttributes.Contact.HomeAddress.PostalCode, sreg.PostalCode); + 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)genderEncoder.Decode(gender); + } + } + + return sreg; + } + + /// <summary> + /// Looks for Simple Registration and Attribute Exchange (all known formats) + /// request extensions and returns them as a Simple Registration extension, + /// and adds the new extension to the original request message if it was absent. + /// </summary> + /// <param name="request">The authentication request.</param> + /// <returns> + /// The Simple Registration request if found, + /// or a fabricated one based on the Attribute Exchange extension if found, + /// or <c>null</c> if no attribute extension request is found.</returns> + internal static ClaimsRequest UnifyExtensionsAsSreg(this Provider.IHostProcessedRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + var req = (Provider.HostProcessedRequest)request; + var sreg = req.GetExtension<ClaimsRequest>(); + if (sreg != null) { + return sreg; + } + + var ax = req.GetExtension<FetchRequest>(); + if (ax != null) { + sreg = new ClaimsRequest(SimpleRegistration.Constants.sreg_ns); + sreg.Synthesized = true; + ((IProtocolMessageWithExtensions)req.RequestMessage).Extensions.Add(sreg); + sreg.BirthDate = GetDemandLevelFor(ax, WellKnownAttributes.BirthDate.WholeBirthDate); + sreg.Country = GetDemandLevelFor(ax, WellKnownAttributes.Contact.HomeAddress.Country); + sreg.Email = GetDemandLevelFor(ax, WellKnownAttributes.Contact.Email); + sreg.FullName = GetDemandLevelFor(ax, WellKnownAttributes.Name.FullName); + sreg.Gender = GetDemandLevelFor(ax, WellKnownAttributes.Person.Gender); + sreg.Language = GetDemandLevelFor(ax, WellKnownAttributes.Preferences.Language); + sreg.Nickname = GetDemandLevelFor(ax, WellKnownAttributes.Name.Alias); + sreg.PostalCode = GetDemandLevelFor(ax, WellKnownAttributes.Contact.HomeAddress.PostalCode); + sreg.TimeZone = GetDemandLevelFor(ax, WellKnownAttributes.Preferences.TimeZone); + } + + return sreg; + } + + /// <summary> + /// Converts the Simple Registration extension response to whatever format the original + /// attribute request extension came in. + /// </summary> + /// <param name="request">The authentication request with the response extensions already added.</param> + /// <remarks> + /// If the original attribute request came in as AX, the Simple Registration extension is converted + /// to an AX response and then the Simple Registration extension is removed from the response. + /// </remarks> + internal static void ConvertSregToMatchRequest(this Provider.IHostProcessedRequest request) { + var req = (Provider.HostProcessedRequest)request; + var response = req.Response as IProtocolMessageWithExtensions; // negative responses don't support extensions. + var sregRequest = request.GetExtension<ClaimsRequest>(); + if (sregRequest != null && response != null) { + if (sregRequest.Synthesized) { + var axRequest = request.GetExtension<FetchRequest>(); + ErrorUtilities.VerifyInternal(axRequest != null, "How do we have a synthesized Sreg request without an AX request?"); + + var sregResponse = response.Extensions.OfType<ClaimsResponse>().SingleOrDefault(); + if (sregResponse == null) { + // No Sreg response to copy from. + return; + } + + // Remove the sreg response since the RP didn't ask for it. + response.Extensions.Remove(sregResponse); + + AXAttributeFormats format = DetectAXFormat(axRequest.Attributes.Select(att => att.TypeUri)); + if (format == AXAttributeFormats.None) { + // No recognized AX attributes were requested. + return; + } + + var axResponse = response.Extensions.OfType<FetchResponse>().SingleOrDefault(); + if (axResponse == null) { + axResponse = new FetchResponse(); + response.Extensions.Add(axResponse); + } + + AddAXAttributeValue(axResponse, WellKnownAttributes.BirthDate.WholeBirthDate, format, sregResponse.BirthDateRaw); + AddAXAttributeValue(axResponse, WellKnownAttributes.Contact.HomeAddress.Country, format, sregResponse.Country); + AddAXAttributeValue(axResponse, WellKnownAttributes.Contact.HomeAddress.PostalCode, format, sregResponse.PostalCode); + AddAXAttributeValue(axResponse, WellKnownAttributes.Contact.Email, format, sregResponse.Email); + AddAXAttributeValue(axResponse, WellKnownAttributes.Name.FullName, format, sregResponse.FullName); + AddAXAttributeValue(axResponse, WellKnownAttributes.Name.Alias, format, sregResponse.Nickname); + AddAXAttributeValue(axResponse, WellKnownAttributes.Preferences.TimeZone, format, sregResponse.TimeZone); + AddAXAttributeValue(axResponse, WellKnownAttributes.Preferences.Language, format, sregResponse.Language); + if (sregResponse.Gender.HasValue) { + AddAXAttributeValue(axResponse, WellKnownAttributes.Person.Gender, format, genderEncoder.Encode(sregResponse.Gender)); + } + } + } + } + + /// <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 ForEachFormat(formats).Select(format => fetchResponse.GetAttributeValue(TransformAXFormat(typeUri, format))).FirstOrDefault(s => s != null); + } + + /// <summary> + /// Transforms an AX attribute type URI from the axschema.org format into a given format. + /// </summary> + /// <param name="axSchemaOrgFormatTypeUri">The ax schema org format type URI.</param> + /// <param name="targetFormat">The target format. Only one flag should be set.</param> + /// <returns>The AX attribute type URI in the target format.</returns> + internal static string TransformAXFormatTestHook(string axSchemaOrgFormatTypeUri, AXAttributeFormats targetFormat) { + return TransformAXFormat(axSchemaOrgFormatTypeUri, targetFormat); + } + + /// <summary> + /// Adds the AX attribute value to the response if it is non-empty. + /// </summary> + /// <param name="ax">The AX Fetch response to add the attribute value to.</param> + /// <param name="typeUri">The attribute type URI in axschema.org format.</param> + /// <param name="format">The target format of the actual attribute to write out.</param> + /// <param name="value">The value of the attribute.</param> + private static void AddAXAttributeValue(FetchResponse ax, string typeUri, AXAttributeFormats format, string value) { + if (!string.IsNullOrEmpty(value)) { + string targetTypeUri = TransformAXFormat(typeUri, format); + if (!ax.Attributes.Contains(targetTypeUri)) { + ax.Attributes.Add(targetTypeUri, value); + } + } + } + + /// <summary> + /// Gets the demand level for an AX attribute. + /// </summary> + /// <param name="ax">The AX fetch request to search for the attribute.</param> + /// <param name="typeUri">The type URI of the attribute in axschema.org format.</param> + /// <returns>The demand level for the attribute.</returns> + private static DemandLevel GetDemandLevelFor(FetchRequest ax, string typeUri) { + Contract.Requires<ArgumentNullException>(ax != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + + foreach (AXAttributeFormats format in ForEachFormat(AXAttributeFormats.All)) { + string typeUriInFormat = TransformAXFormat(typeUri, format); + if (ax.Attributes.Contains(typeUriInFormat)) { + return ax.Attributes[typeUriInFormat].IsRequired ? DemandLevel.Require : DemandLevel.Request; + } + } + + return DemandLevel.NoRequest; + } + + /// <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 = DetectAXFormat(request.DiscoveryResult.Capabilities); + return attributeFormat != AXAttributeFormats.None; + } + + /// <summary> + /// Detects the AX attribute type URI format from a given sample. + /// </summary> + /// <param name="typeURIs">The type URIs to scan for recognized formats.</param> + /// <returns>The first AX type URI format recognized in the list.</returns> + private static AXAttributeFormats DetectAXFormat(IEnumerable<string> typeURIs) { + Contract.Requires<ArgumentNullException>(typeURIs != null); + + if (typeURIs.Any(uri => uri.StartsWith("http://axschema.org/", StringComparison.Ordinal))) { + return AXAttributeFormats.AXSchemaOrg; + } + + if (typeURIs.Any(uri => uri.StartsWith("http://schema.openid.net/", StringComparison.Ordinal))) { + return AXAttributeFormats.SchemaOpenIdNet; + } + + if (typeURIs.Any(uri => uri.StartsWith("http://openid.net/schema/", StringComparison.Ordinal))) { + return AXAttributeFormats.OpenIdNetSchema; + } + + return AXAttributeFormats.None; + } + + /// <summary> + /// Transforms an AX attribute type URI from the axschema.org format into a given format. + /// </summary> + /// <param name="axSchemaOrgFormatTypeUri">The ax schema org format type URI.</param> + /// <param name="targetFormat">The target format. Only one flag should be set.</param> + /// <returns>The AX attribute type URI in the target format.</returns> + private static string TransformAXFormat(string axSchemaOrgFormatTypeUri, AXAttributeFormats targetFormat) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(axSchemaOrgFormatTypeUri)); + + switch (targetFormat) { + case AXAttributeFormats.AXSchemaOrg: + return axSchemaOrgFormatTypeUri; + case AXAttributeFormats.SchemaOpenIdNet: + return axSchemaOrgFormatTypeUri.Replace("axschema.org", "schema.openid.net"); + case AXAttributeFormats.OpenIdNetSchema: + return axSchemaOrgFormatTypeUri.Replace("axschema.org", "openid.net/schema"); + default: + throw new ArgumentOutOfRangeException("targetFormat"); + } + } + + /// <summary> + /// Splits the AX attribute format flags into individual values for processing. + /// </summary> + /// <param name="formats">The formats to split up into individual flags.</param> + /// <returns>A sequence of individual flags.</returns> + private static IEnumerable<AXAttributeFormats> ForEachFormat(AXAttributeFormats formats) { + if ((formats & AXAttributeFormats.AXSchemaOrg) != 0) { + yield return AXAttributeFormats.AXSchemaOrg; + } + + if ((formats & AXAttributeFormats.OpenIdNetSchema) != 0) { + yield return AXAttributeFormats.OpenIdNetSchema; + } + + if ((formats & AXAttributeFormats.SchemaOpenIdNet) != 0) { + yield return AXAttributeFormats.SchemaOpenIdNet; + } + } + + /// <summary> + /// Adds an attribute fetch request if it is not already present in the AX request. + /// </summary> + /// <param name="ax">The AX request to add the attribute request to.</param> + /// <param name="format">The format of the attribute's Type URI to use.</param> + /// <param name="axSchemaOrgFormatAttribute">The attribute in axschema.org format.</param> + /// <param name="demandLevel">The demand level.</param> + private static void FetchAttribute(FetchRequest ax, AXAttributeFormats format, string axSchemaOrgFormatAttribute, DemandLevel demandLevel) { + Contract.Requires<ArgumentNullException>(ax != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(axSchemaOrgFormatAttribute)); + + string typeUri = TransformAXFormat(axSchemaOrgFormatAttribute, format); + if (!ax.Attributes.Contains(typeUri)) { + switch (demandLevel) { + case DemandLevel.Request: + ax.Attributes.AddOptional(typeUri); + break; + case DemandLevel.Require: + ax.Attributes.AddRequired(typeUri); + break; + default: + break; + } + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs new file mode 100644 index 0000000..c84f507 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/IClientScriptExtensionResponse.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="IClientScriptExtensionResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions { + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An interface that OpenID extensions can implement to allow authentication response + /// messages with included extensions to be processed by Javascript on the user agent. + /// </summary> + public interface IClientScriptExtensionResponse : IExtensionMessage { + /// <summary> + /// Reads the extension information on an authentication response from the provider. + /// </summary> + /// <param name="response">The incoming OpenID response carrying the extension.</param> + /// <returns> + /// A Javascript snippet that when executed on the user agent returns an object with + /// the information deserialized from the extension response. + /// </returns> + /// <remarks> + /// This method is called <b>before</b> the signature on the assertion response has been + /// verified. Therefore all information in these fields should be assumed unreliable + /// and potentially falsified. + /// </remarks> + string InitializeJavaScriptData(IProtocolMessageWithExtensions response); + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs new file mode 100644 index 0000000..5e7bc49 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationApprovedResponse.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationApprovedResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.OAuth { + using System; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The OAuth response that a Provider may include with a positive + /// OpenID identity assertion with an approved request token. + /// </summary> + [Serializable] + public class AuthorizationApprovedResponse : ExtensionBase { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && !isProviderRole && data.ContainsKey(Constants.RequestTokenParameter)) { + return new AuthorizationApprovedResponse(); + } + + return null; + }; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationApprovedResponse"/> class. + /// </summary> + public AuthorizationApprovedResponse() + : base(new Version(1, 0), Constants.TypeUri, null) { + } + + /// <summary> + /// Gets or sets the user-approved request token. + /// </summary> + /// <value>The request token.</value> + [MessagePart(Constants.RequestTokenParameter, IsRequired = true, AllowEmpty = false)] + public string RequestToken { get; set; } + + /// <summary> + /// Gets or sets a string that encodes, in a way possibly specific to the Combined Provider, one or more scopes that the returned request token is valid for. This will typically indicate a subset of the scopes requested in Section 8. + /// </summary> + [MessagePart("scope", IsRequired = false, AllowEmpty = true)] + public string Scope { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs new file mode 100644 index 0000000..7c3a5ad --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationDeclinedResponse.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationDeclinedResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.OAuth { + using System; + + /// <summary> + /// The OAuth response that a Provider should include with a positive + /// OpenID identity assertion when OAuth authorization was declined. + /// </summary> + [Serializable] + public class AuthorizationDeclinedResponse : ExtensionBase { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && !isProviderRole && !data.ContainsKey(Constants.RequestTokenParameter)) { + return new AuthorizationDeclinedResponse(); + } + + return null; + }; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationDeclinedResponse"/> class. + /// </summary> + public AuthorizationDeclinedResponse() + : base(new Version(1, 0), Constants.TypeUri, null) { + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs new file mode 100644 index 0000000..99f0880 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/AuthorizationRequest.cs @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.OAuth { + using System; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An extension to include with an authentication request in order to also + /// obtain authorization to access user data at the combined OpenID Provider + /// and Service Provider. + /// </summary> + /// <remarks> + /// <para>When requesting OpenID Authentication via the protocol mode "checkid_setup" + /// or "checkid_immediate", this extension can be used to request that the end + /// user authorize an OAuth access token at the same time as an OpenID + /// authentication. This is done by sending the following parameters as part + /// of the OpenID request. (Note that the use of "oauth" as part of the parameter + /// names here and in subsequent sections is just an example. See Section 5 for details.)</para> + /// <para>See section 8.</para> + /// </remarks> + [Serializable] + public class AuthorizationRequest : ExtensionBase { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && isProviderRole) { + return new AuthorizationRequest(); + } + + return null; + }; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationRequest"/> class. + /// </summary> + public AuthorizationRequest() + : base(new Version(1, 0), Constants.TypeUri, null) { + } + + /// <summary> + /// Gets or sets the consumer key agreed upon between the Consumer and Service Provider. + /// </summary> + [MessagePart("consumer", IsRequired = true, AllowEmpty = false)] + public string Consumer { get; set; } + + /// <summary> + /// Gets or sets a string that encodes, in a way possibly specific to the Combined Provider, one or more scopes for the OAuth token expected in the authentication response. + /// </summary> + [MessagePart("scope", IsRequired = false)] + public string Scope { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs new file mode 100644 index 0000000..32efee9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OAuth/Constants.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// <copyright file="Constants.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.OAuth { + /// <summary> + /// Constants used in the OpenID OAuth extension. + /// </summary> + internal static class Constants { + /// <summary> + /// The TypeURI for the OpenID OAuth extension. + /// </summary> + internal const string TypeUri = "http://specs.openid.net/extensions/oauth/1.0"; + + /// <summary> + /// The name of the parameter that carries the request token in the response. + /// </summary> + internal const string RequestTokenParameter = "request_token"; + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs new file mode 100644 index 0000000..05e6687 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/OpenIdExtensionFactoryAggregator.cs @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdExtensionFactoryAggregator.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions { + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// An OpenID extension factory that only delegates extension + /// instantiation requests to other factories. + /// </summary> + internal class OpenIdExtensionFactoryAggregator : IOpenIdExtensionFactory { + /// <summary> + /// The list of factories this factory delegates to. + /// </summary> + private List<IOpenIdExtensionFactory> factories = new List<IOpenIdExtensionFactory>(2); + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdExtensionFactoryAggregator"/> class. + /// </summary> + internal OpenIdExtensionFactoryAggregator() { + } + + /// <summary> + /// Gets the extension factories that this aggregating factory delegates to. + /// </summary> + /// <value>A list of factories. May be empty, but never null.</value> + internal IList<IOpenIdExtensionFactory> Factories { + get { return this.factories; } + } + + #region IOpenIdExtensionFactory Members + + /// <summary> + /// Creates a new instance of some extension based on the received extension parameters. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + /// <remarks> + /// This factory method need only initialize properties in the instantiated extension object + /// that are not bound using <see cref="MessagePartAttribute"/>. + /// </remarks> + public IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole) { + foreach (var factory in this.factories) { + IOpenIdMessageExtension result = factory.Create(typeUri, data, baseMessage, isProviderRole); + if (result != null) { + return result; + } + } + + return null; + } + + #endregion + + /// <summary> + /// Loads the default factory and additional ones given by the configuration. + /// </summary> + /// <returns>A new instance of <see cref="OpenIdExtensionFactoryAggregator"/>.</returns> + internal static OpenIdExtensionFactoryAggregator LoadFromConfiguration() { + Contract.Ensures(Contract.Result<OpenIdExtensionFactoryAggregator>() != null); + var factoriesElement = DotNetOpenAuth.Configuration.DotNetOpenAuthSection.Configuration.OpenId.ExtensionFactories; + var aggregator = new OpenIdExtensionFactoryAggregator(); + aggregator.Factories.Add(new StandardOpenIdExtensionFactory()); + aggregator.factories.AddRange(factoriesElement.CreateInstances(false)); + return aggregator; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs new file mode 100644 index 0000000..99c7a2e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationPolicies.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System.Diagnostics.CodeAnalysis; + + /// <summary> + /// Well-known authentication policies defined in the PAPE extension spec or by a recognized + /// standards body. + /// </summary> + /// <remarks> + /// This is a class of constants rather than a flags enum because policies may be + /// freely defined and used by anyone, just by using a new Uri. + /// </remarks> + public static class AuthenticationPolicies { + /// <summary> + /// An authentication mechanism where the End User does not provide a shared secret to a party potentially under the control of the Relying Party. (Note that the potentially malicious Relying Party controls where the User-Agent is redirected to and thus may not send it to the End User's actual OpenID Provider). + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Phishing", Justification = "By design")] + public const string PhishingResistant = "http://schemas.openid.net/pape/policies/2007/06/phishing-resistant"; + + /// <summary> + /// An authentication mechanism where the End User authenticates to the OpenID Provider by providing over one authentication factor. Common authentication factors are something you know, something you have, and something you are. An example would be authentication using a password and a software token or digital certificate. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "By design")] + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "MultiFactor", Justification = "By design")] + public const string MultiFactor = "http://schemas.openid.net/pape/policies/2007/06/multi-factor"; + + /// <summary> + /// An authentication mechanism where the End User authenticates to the OpenID Provider by providing over one authentication factor where at least one of the factors is a physical factor such as a hardware device or biometric. Common authentication factors are something you know, something you have, and something you are. This policy also implies the Multi-Factor Authentication policy (http://schemas.openid.net/pape/policies/2007/06/multi-factor) and both policies MAY BE specified in conjunction without conflict. An example would be authentication using a password and a hardware token. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "MultiFactor", Justification = "By design")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Multi", Justification = "By design")] + public const string PhysicalMultiFactor = "http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical"; + + /// <summary> + /// Indicates that the Provider MUST use a pair-wise pseudonym for the user that is persistent + /// and unique across the requesting realm as the openid.claimed_id and openid.identity (see Section 4.2). + /// </summary> + public const string PrivatePersonalIdentifier = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier"; + + /// <summary> + /// Indicates that the OP MUST only respond with a positive assertion if the requirements demonstrated + /// by the OP to obtain certification by a Federally adopted Trust Framework Provider have been met. + /// </summary> + /// <remarks> + /// Notwithstanding the RP may request this authentication policy, the RP MUST still + /// verify that this policy appears in the positive assertion response rather than assume the OP + /// recognized and complied with the request. + /// </remarks> + public const string USGovernmentTrustLevel1 = "http://www.idmanagement.gov/schema/2009/05/icam/openid-trust-level1.pdf"; + + /// <summary> + /// Indicates that the OP MUST not include any OpenID Attribute Exchange or Simple Registration + /// information regarding the user in the assertion. + /// </summary> + public const string NoPersonallyIdentifiableInformation = "http://www.idmanagement.gov/schema/2009/05/icam/no-pii.pdf"; + + /// <summary> + /// Used in a PAPE response to indicate that no PAPE authentication policies could be satisfied. + /// </summary> + /// <remarks> + /// Used internally by the PAPE extension, so that users don't have to know about it. + /// </remarks> + internal const string None = "http://schemas.openid.net/pape/policies/2007/06/none"; + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs new file mode 100644 index 0000000..93e76d5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------- +// <copyright file="Constants.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System; + using System.Collections.Generic; + using System.Text; + + /// <summary> + /// OpenID Provider Authentication Policy extension constants. + /// </summary> + internal static class Constants { + /// <summary> + /// The namespace used by this extension in messages. + /// </summary> + internal const string TypeUri = "http://specs.openid.net/extensions/pape/1.0"; + + /// <summary> + /// The namespace alias to use for OpenID 1.x interop, where aliases are not defined in the message. + /// </summary> + internal const string CompatibilityAlias = "pape"; + + /// <summary> + /// The string to prepend on an Auth Level Type alias definition. + /// </summary> + internal const string AuthLevelNamespaceDeclarationPrefix = "auth_level.ns."; + + /// <summary> + /// Well-known assurance level Type URIs. + /// </summary> + internal static class AssuranceLevels { + /// <summary> + /// A mapping between the PAPE TypeURI and the alias to use if + /// possible for backward compatibility reasons. + /// </summary> + internal static readonly IDictionary<string, string> PreferredTypeUriToAliasMap = new Dictionary<string, string> { + { NistTypeUri, "nist" }, + }; + + /// <summary> + /// The Type URI of the NIST assurance level. + /// </summary> + internal const string NistTypeUri = "http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf"; + } + + /// <summary> + /// Parameters to be included with PAPE requests. + /// </summary> + internal static class RequestParameters { + /// <summary> + /// Optional. If the End User has not actively authenticated to the OP within the number of seconds specified in a manner fitting the requested policies, the OP SHOULD authenticate the End User for this request. + /// </summary> + /// <value>Integer value greater than or equal to zero in seconds.</value> + /// <remarks> + /// The OP should realize that not adhering to the request for re-authentication most likely means that the End User will not be allowed access to the services provided by the RP. If this parameter is absent in the request, the OP should authenticate the user at its own discretion. + /// </remarks> + internal const string MaxAuthAge = "max_auth_age"; + + /// <summary> + /// Zero or more authentication policy URIs that the OP SHOULD conform to when authenticating the user. If multiple policies are requested, the OP SHOULD satisfy as many as it can. + /// </summary> + /// <value>Space separated list of authentication policy URIs.</value> + /// <remarks> + /// If no policies are requested, the RP may be interested in other information such as the authentication age. + /// </remarks> + internal const string PreferredAuthPolicies = "preferred_auth_policies"; + + /// <summary> + /// The space separated list of the name spaces of the custom Assurance Level that RP requests, in the order of its preference. + /// </summary> + internal const string PreferredAuthLevelTypes = "preferred_auth_level_types"; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs new file mode 100644 index 0000000..9dc0574 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/DateTimeEncoder.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="DateTimeEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System; + using System.Globalization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// An encoder/decoder design for DateTimes that must conform to the PAPE spec. + /// </summary> + /// <remarks> + /// The timestamp MUST be formatted as specified in section 5.6 of [RFC3339] (Klyne, G. and C. Newman, “Date and Time on the Internet: Timestamps,” .), with the following restrictions: + /// * All times must be in the UTC timezone, indicated with a "Z". + /// * No fractional seconds are allowed + /// For example: 2005-05-15T17:11:51Z + /// </remarks> + internal class DateTimeEncoder : IMessagePartEncoder { + /// <summary> + /// An array of the date/time formats allowed by the PAPE extension. + /// </summary> + /// <remarks> + /// TODO: This array of formats is not yet a complete list. + /// </remarks> + private static readonly string[] PermissibleDateTimeFormats = { "yyyy-MM-ddTHH:mm:ssZ" }; + + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + DateTime? dateTime = value as DateTime?; + if (dateTime.HasValue) { + return dateTime.Value.ToUniversalTimeSafe().ToString(PermissibleDateTimeFormats[0], CultureInfo.InvariantCulture); + } else { + return null; + } + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + DateTime dateTime; + if (DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out dateTime) && dateTime.Kind == DateTimeKind.Utc) { // may be unspecified per our option above + return dateTime; + } else { + Logger.OpenId.ErrorFormat("Invalid format for message part: {0}", value); + return null; + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs new file mode 100644 index 0000000..3031aad --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="NistAssuranceLevel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Text; + + /// <summary> + /// Descriptions for NIST-defined levels of assurance that a credential + /// has not been compromised and therefore the extent to which an + /// authentication assertion can be trusted. + /// </summary> + /// <remarks> + /// <para>One using this enum should review the following publication for details + /// before asserting or interpreting what these levels signify, notwithstanding + /// the brief summaries attached to each level in DotNetOpenAuth documentation. + /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf</para> + /// <para> + /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels. + /// </para> + /// </remarks> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nist", Justification = "By design")] + public enum NistAssuranceLevel { + /// <summary> + /// Not an assurance level defined by NIST, but rather SHOULD be used to + /// signify that the OP recognizes the parameter and the End User + /// authentication did not meet the requirements of Level 1. + /// </summary> + InsufficientForLevel1 = 0, + + /// <summary> + /// See this document for a thorough description: + /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf + /// </summary> + Level1 = 1, + + /// <summary> + /// See this document for a thorough description: + /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf + /// </summary> + Level2 = 2, + + /// <summary> + /// See this document for a thorough description: + /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf + /// </summary> + Level3 = 3, + + /// <summary> + /// See this document for a thorough description: + /// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf + /// </summary> + Level4 = 4, + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs new file mode 100644 index 0000000..eeaea31 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PapeUtilities.cs @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------- +// <copyright file="PapeUtilities.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Utility methods for use by the PAPE extension. + /// </summary> + internal static class PapeUtilities { + /// <summary> + /// Looks at the incoming fields and figures out what the aliases and name spaces for auth level types are. + /// </summary> + /// <param name="fields">The incoming message data in which to discover TypeURIs and aliases.</param> + /// <returns>The <see cref="AliasManager"/> initialized with the given data.</returns> + internal static AliasManager FindIncomingAliases(IDictionary<string, string> fields) { + AliasManager aliasManager = new AliasManager(); + + foreach (var pair in fields) { + if (!pair.Key.StartsWith(Constants.AuthLevelNamespaceDeclarationPrefix, StringComparison.Ordinal)) { + continue; + } + + string alias = pair.Key.Substring(Constants.AuthLevelNamespaceDeclarationPrefix.Length); + aliasManager.SetAlias(alias, pair.Value); + } + + aliasManager.SetPreferredAliasesWhereNotSet(Constants.AssuranceLevels.PreferredTypeUriToAliasMap); + + return aliasManager; + } + + /// <summary> + /// Concatenates a sequence of strings using a space as a separator. + /// </summary> + /// <param name="values">The elements to concatenate together..</param> + /// <returns>The concatenated string of elements.</returns> + /// <exception cref="FormatException">Thrown if any element in the sequence includes a space.</exception> + internal static string ConcatenateListOfElements(IEnumerable<string> values) { + Contract.Requires<ArgumentNullException>(values != null); + + StringBuilder valuesList = new StringBuilder(); + foreach (string value in values.Distinct()) { + if (value.Contains(" ")) { + throw new FormatException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidUri, value)); + } + valuesList.Append(value); + valuesList.Append(" "); + } + if (valuesList.Length > 0) { + valuesList.Length -= 1; // remove trailing space + } + return valuesList.ToString(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs new file mode 100644 index 0000000..84589dc --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs @@ -0,0 +1,222 @@ +//----------------------------------------------------------------------- +// <copyright file="PolicyRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The PAPE request part of an OpenID Authentication request message. + /// </summary> + [Serializable] + public sealed class PolicyRequest : ExtensionBase, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && isProviderRole) { + return new PolicyRequest(); + } + + return null; + }; + + /// <summary> + /// The transport field for the RP's preferred authentication policies. + /// </summary> + /// <remarks> + /// This field is written to/read from during custom serialization. + /// </remarks> + [MessagePart("preferred_auth_policies", IsRequired = true)] + private string preferredPoliciesString; + + /// <summary> + /// Initializes a new instance of the <see cref="PolicyRequest"/> class. + /// </summary> + public PolicyRequest() + : base(new Version(1, 0), Constants.TypeUri, null) { + this.PreferredPolicies = new List<string>(1); + this.PreferredAuthLevelTypes = new List<string>(1); + } + + /// <summary> + /// Gets or sets the maximum acceptable time since the End User has + /// actively authenticated to the OP in a manner fitting the requested + /// policies, beyond which the Provider SHOULD authenticate the + /// End User for this request. + /// </summary> + /// <remarks> + /// The OP should realize that not adhering to the request for re-authentication + /// most likely means that the End User will not be allowed access to the + /// services provided by the RP. If this parameter is absent in the request, + /// the OP should authenticate the user at its own discretion. + /// </remarks> + [MessagePart("max_auth_age", IsRequired = false, Encoder = typeof(TimespanSecondsEncoder))] + public TimeSpan? MaximumAuthenticationAge { get; set; } + + /// <summary> + /// Gets the list of authentication policy URIs that the OP SHOULD + /// conform to when authenticating the user. If multiple policies are + /// requested, the OP SHOULD satisfy as many as it can. + /// </summary> + /// <value>List of authentication policy URIs obtainable from + /// the <see cref="AuthenticationPolicies"/> class or from a custom + /// list.</value> + /// <remarks> + /// If no policies are requested, the RP may be interested in other + /// information such as the authentication age. + /// </remarks> + public IList<string> PreferredPolicies { get; private set; } + + /// <summary> + /// Gets the namespaces of the custom Assurance Level the + /// Relying Party requests, in the order of its preference. + /// </summary> + public IList<string> PreferredAuthLevelTypes { get; private set; } + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + var extraData = ((IMessage)this).ExtraData; + extraData.Clear(); + + this.preferredPoliciesString = SerializePolicies(this.PreferredPolicies); + + if (this.PreferredAuthLevelTypes.Count > 0) { + AliasManager authLevelAliases = new AliasManager(); + authLevelAliases.AssignAliases(this.PreferredAuthLevelTypes, Constants.AssuranceLevels.PreferredTypeUriToAliasMap); + + // Add a definition for each Auth Level Type alias. + foreach (string alias in authLevelAliases.Aliases) { + extraData.Add(Constants.AuthLevelNamespaceDeclarationPrefix + alias, authLevelAliases.ResolveAlias(alias)); + } + + // Now use the aliases for those type URIs to list a preferred order. + extraData.Add(Constants.RequestParameters.PreferredAuthLevelTypes, SerializeAuthLevels(this.PreferredAuthLevelTypes, authLevelAliases)); + } + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + var extraData = ((IMessage)this).ExtraData; + + this.PreferredPolicies.Clear(); + string[] preferredPolicies = this.preferredPoliciesString.Split(' '); + foreach (string policy in preferredPolicies) { + if (policy.Length > 0) { + this.PreferredPolicies.Add(policy); + } + } + + this.PreferredAuthLevelTypes.Clear(); + AliasManager authLevelAliases = PapeUtilities.FindIncomingAliases(extraData); + string preferredAuthLevelAliases; + if (extraData.TryGetValue(Constants.RequestParameters.PreferredAuthLevelTypes, out preferredAuthLevelAliases)) { + foreach (string authLevelAlias in preferredAuthLevelAliases.Split(' ')) { + if (authLevelAlias.Length == 0) { + continue; + } + this.PreferredAuthLevelTypes.Add(authLevelAliases.ResolveAlias(authLevelAlias)); + } + } + } + + #endregion + + /// <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) { + PolicyRequest other = obj as PolicyRequest; + if (other == null) { + return false; + } + + if (this.MaximumAuthenticationAge != other.MaximumAuthenticationAge) { + return false; + } + + if (this.PreferredPolicies.Count != other.PreferredPolicies.Count) { + return false; + } + + foreach (string policy in this.PreferredPolicies) { + if (!other.PreferredPolicies.Contains(policy)) { + return false; + } + } + + if (this.PreferredAuthLevelTypes.Count != other.PreferredAuthLevelTypes.Count) { + return false; + } + + foreach (string authLevel in this.PreferredAuthLevelTypes) { + if (!other.PreferredAuthLevelTypes.Contains(authLevel)) { + return false; + } + } + + return true; + } + + /// <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() { + // This is a poor hash function, but an site that cares will likely have a bunch + // of look-alike instances anyway, so a good hash function would still bunch + // all the instances into the same hash code. + if (this.MaximumAuthenticationAge.HasValue) { + return this.MaximumAuthenticationAge.Value.GetHashCode(); + } else { + return 1; + } + } + + /// <summary> + /// Serializes the policies as a single string per the PAPE spec.. + /// </summary> + /// <param name="policies">The policies to include in the list.</param> + /// <returns>The concatenated string of the given policies.</returns> + private static string SerializePolicies(IEnumerable<string> policies) { + return PapeUtilities.ConcatenateListOfElements(policies); + } + + /// <summary> + /// Serializes the auth levels to a list of aliases. + /// </summary> + /// <param name="preferredAuthLevelTypes">The preferred auth level types.</param> + /// <param name="aliases">The alias manager.</param> + /// <returns>A space-delimited list of aliases.</returns> + private static string SerializeAuthLevels(IList<string> preferredAuthLevelTypes, AliasManager aliases) { + var aliasList = new List<string>(); + foreach (string typeUri in preferredAuthLevelTypes) { + aliasList.Add(aliases.GetAlias(typeUri)); + } + + return PapeUtilities.ConcatenateListOfElements(aliasList); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs new file mode 100644 index 0000000..246ec07 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs @@ -0,0 +1,282 @@ +//----------------------------------------------------------------------- +// <copyright file="PolicyResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// The PAPE response part of an OpenID Authentication response message. + /// </summary> + [Serializable] + public sealed class PolicyResponse : ExtensionBase, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.TypeUri && !isProviderRole) { + return new PolicyResponse(); + } + + return null; + }; + + /// <summary> + /// The first part of a parameter name that gives the custom string value for + /// the assurance level. The second part of the parameter name is the alias for + /// that assurance level. + /// </summary> + private const string AuthLevelAliasPrefix = "auth_level."; + + /// <summary> + /// One or more authentication policy URIs that the OP conformed to when authenticating the End User. + /// </summary> + /// <value>Space separated list of authentication policy URIs.</value> + /// <remarks> + /// If no policies were met though the OP wishes to convey other information in the response, this parameter MUST be included with the value of "none". + /// </remarks> + [MessagePart("auth_policies", IsRequired = true)] + private string actualPoliciesString; + + /// <summary> + /// Backing field for the <see cref="AuthenticationTimeUtc"/> property. + /// </summary> + private DateTime? authenticationTimeUtc; + + /// <summary> + /// Initializes a new instance of the <see cref="PolicyResponse"/> class. + /// </summary> + public PolicyResponse() + : base(new Version(1, 0), Constants.TypeUri, null) { + this.ActualPolicies = new List<string>(1); + this.AssuranceLevels = new Dictionary<string, string>(1); + } + + /// <summary> + /// Gets a list of authentication policy URIs that the + /// OP conformed to when authenticating the End User. + /// </summary> + public IList<string> ActualPolicies { get; private set; } + + /// <summary> + /// Gets or sets the most recent timestamp when the End User has + /// actively authenticated to the OP in a manner fitting the asserted policies. + /// </summary> + /// <remarks> + /// If the RP's request included the "openid.max_auth_age" parameter + /// then the OP MUST include "openid.auth_time" in its response. + /// If "openid.max_auth_age" was not requested, the OP MAY choose to include + /// "openid.auth_time" in its response. + /// </remarks> + [MessagePart("auth_time", Encoder = typeof(DateTimeEncoder))] + public DateTime? AuthenticationTimeUtc { + get { + return this.authenticationTimeUtc; + } + + set { + Contract.Requires<ArgumentException>(!value.HasValue || value.Value.Kind != DateTimeKind.Unspecified, OpenIdStrings.UnspecifiedDateTimeKindNotAllowed); + + // Make sure that whatever is set here, it becomes UTC time. + if (value.HasValue) { + // Convert to UTC and cut to the second, since the protocol only allows for + // that level of precision. + this.authenticationTimeUtc = OpenIdUtilities.CutToSecond(value.Value.ToUniversalTimeSafe()); + } else { + this.authenticationTimeUtc = null; + } + } + } + + /// <summary> + /// Gets or sets the Assurance Level as defined by the National + /// Institute of Standards and Technology (NIST) in Special Publication + /// 800-63 (Burr, W., Dodson, D., and W. Polk, Ed., “Electronic + /// Authentication Guideline,” April 2006.) [NIST_SP800‑63] corresponding + /// to the authentication method and policies employed by the OP when + /// authenticating the End User. + /// </summary> + /// <remarks> + /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level + /// example classifications of authentication methods within the defined + /// levels. + /// </remarks> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nist", Justification = "Acronym")] + public NistAssuranceLevel? NistAssuranceLevel { + get { + string levelString; + if (this.AssuranceLevels.TryGetValue(Constants.AssuranceLevels.NistTypeUri, out levelString)) { + return (NistAssuranceLevel)Enum.Parse(typeof(NistAssuranceLevel), levelString); + } else { + return null; + } + } + + set { + if (value != null) { + this.AssuranceLevels[Constants.AssuranceLevels.NistTypeUri] = ((int)value).ToString(CultureInfo.InvariantCulture); + } else { + this.AssuranceLevels.Remove(Constants.AssuranceLevels.NistTypeUri); + } + } + } + + /// <summary> + /// Gets a dictionary where keys are the authentication level type URIs and + /// the values are the per authentication level defined custom value. + /// </summary> + /// <remarks> + /// A very common key is <see cref="Constants.AssuranceLevels.NistTypeUri"/> + /// and values for this key are available in <see cref="NistAssuranceLevel"/>. + /// </remarks> + public IDictionary<string, string> AssuranceLevels { get; private set; } + + /// <summary> + /// Gets a value indicating whether this extension is signed by the Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>. + /// </value> + public bool IsSignedByProvider { + get { return this.IsSignedByRemoteParty; } + } + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + var extraData = ((IMessage)this).ExtraData; + extraData.Clear(); + + this.actualPoliciesString = SerializePolicies(this.ActualPolicies); + + if (this.AssuranceLevels.Count > 0) { + AliasManager aliases = new AliasManager(); + aliases.AssignAliases(this.AssuranceLevels.Keys, Constants.AssuranceLevels.PreferredTypeUriToAliasMap); + + // Add a definition for each Auth Level Type alias. + foreach (string alias in aliases.Aliases) { + extraData.Add(Constants.AuthLevelNamespaceDeclarationPrefix + alias, aliases.ResolveAlias(alias)); + } + + // Now use the aliases for those type URIs to list the individual values. + foreach (var pair in this.AssuranceLevels) { + extraData.Add(AuthLevelAliasPrefix + aliases.GetAlias(pair.Key), pair.Value); + } + } + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + var extraData = ((IMessage)this).ExtraData; + + this.ActualPolicies.Clear(); + string[] actualPolicies = this.actualPoliciesString.Split(' '); + foreach (string policy in actualPolicies) { + if (policy.Length > 0 && policy != AuthenticationPolicies.None) { + this.ActualPolicies.Add(policy); + } + } + + this.AssuranceLevels.Clear(); + AliasManager authLevelAliases = PapeUtilities.FindIncomingAliases(extraData); + foreach (string authLevelAlias in authLevelAliases.Aliases) { + string authValue; + if (extraData.TryGetValue(AuthLevelAliasPrefix + authLevelAlias, out authValue)) { + string authLevelType = authLevelAliases.ResolveAlias(authLevelAlias); + this.AssuranceLevels[authLevelType] = authValue; + } + } + } + + #endregion + + /// <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) { + PolicyResponse other = obj as PolicyResponse; + if (other == null) { + return false; + } + + if (this.AuthenticationTimeUtc != other.AuthenticationTimeUtc) { + return false; + } + + if (this.AssuranceLevels.Count != other.AssuranceLevels.Count) { + return false; + } + + foreach (var pair in this.AssuranceLevels) { + if (!other.AssuranceLevels.Contains(pair)) { + return false; + } + } + + if (this.ActualPolicies.Count != other.ActualPolicies.Count) { + return false; + } + + foreach (string policy in this.ActualPolicies) { + if (!other.ActualPolicies.Contains(policy)) { + return false; + } + } + + return true; + } + + /// <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() { + // This is a poor hash function, but an site that cares will likely have a bunch + // of look-alike instances anyway, so a good hash function would still bunch + // all the instances into the same hash code. + if (this.AuthenticationTimeUtc.HasValue) { + return this.AuthenticationTimeUtc.Value.GetHashCode(); + } else { + return 1; + } + } + + /// <summary> + /// Serializes the applied policies for transmission from the Provider + /// to the Relying Party. + /// </summary> + /// <param name="policies">The applied policies.</param> + /// <returns>A space-delimited list of applied policies.</returns> + private static string SerializePolicies(IList<string> policies) { + if (policies.Count == 0) { + return AuthenticationPolicies.None; + } else { + return PapeUtilities.ConcatenateListOfElements(policies); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs new file mode 100644 index 0000000..cec8042 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs @@ -0,0 +1,316 @@ +//----------------------------------------------------------------------- +// <copyright file="ClaimsRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Carries the request/require/none demand state of the simple registration fields. + /// </summary> + [Serializable] + public sealed class ClaimsRequest : ExtensionBase { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == Constants.sreg_ns && isProviderRole) { + return new ClaimsRequest(typeUri); + } + + return null; + }; + + /// <summary> + /// The type URI that this particular (deserialized) extension was read in using, + /// allowing a response to alter be crafted using the same type URI. + /// </summary> + private string typeUriDeserializedFrom; + + /// <summary> + /// Initializes a new instance of the <see cref="ClaimsRequest"/> class. + /// </summary> + public ClaimsRequest() + : base(new Version(1, 0), Constants.sreg_ns, Constants.AdditionalTypeUris) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ClaimsRequest"/> class + /// by deserializing from a message. + /// </summary> + /// <param name="typeUri">The type URI this extension was recognized by in the OpenID message.</param> + internal ClaimsRequest(string typeUri) + : this() { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUri)); + + this.typeUriDeserializedFrom = typeUri; + } + + /// <summary> + /// Gets or sets the URL the consumer site provides for the authenticating user to review + /// for how his claims will be used by the consumer web site. + /// </summary> + [MessagePart(Constants.policy_url, IsRequired = false)] + public Uri PolicyUrl { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the nickname of the user. + /// </summary> + public DemandLevel Nickname { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the email of the user. + /// </summary> + public DemandLevel Email { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the full name of the user. + /// </summary> + public DemandLevel FullName { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the birthdate of the user. + /// </summary> + public DemandLevel BirthDate { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the gender of the user. + /// </summary> + public DemandLevel Gender { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the postal code of the user. + /// </summary> + public DemandLevel PostalCode { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the Country of the user. + /// </summary> + public DemandLevel Country { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the language of the user. + /// </summary> + public DemandLevel Language { get; set; } + + /// <summary> + /// Gets or sets the level of interest a relying party has in the time zone of the user. + /// </summary> + public DemandLevel TimeZone { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="ClaimsRequest"/> instance + /// is synthesized from an AX request at the Provider. + /// </summary> + internal bool Synthesized { get; set; } + + /// <summary> + /// Gets or sets the value of the sreg.required parameter. + /// </summary> + /// <value>A comma-delimited list of sreg fields.</value> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")] + [MessagePart(Constants.required, AllowEmpty = true)] + private string RequiredList { + get { return string.Join(",", this.AssembleProfileFields(DemandLevel.Require)); } + set { this.SetProfileRequestFromList(value.Split(','), DemandLevel.Require); } + } + + /// <summary> + /// Gets or sets the value of the sreg.optional parameter. + /// </summary> + /// <value>A comma-delimited list of sreg fields.</value> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")] + [MessagePart(Constants.optional, AllowEmpty = true)] + private string OptionalList { + get { return string.Join(",", this.AssembleProfileFields(DemandLevel.Request)); } + set { this.SetProfileRequestFromList(value.Split(','), DemandLevel.Request); } + } + + /// <summary> + /// Tests equality between two <see cref="ClaimsRequest"/> structs. + /// </summary> + /// <param name="one">One instance to compare.</param> + /// <param name="other">Another instance to compare.</param> + /// <returns>The result of the operator.</returns> + public static bool operator ==(ClaimsRequest one, ClaimsRequest other) { + return one.EqualsNullSafe(other); + } + + /// <summary> + /// Tests inequality between two <see cref="ClaimsRequest"/> structs. + /// </summary> + /// <param name="one">One instance to compare.</param> + /// <param name="other">Another instance to compare.</param> + /// <returns>The result of the operator.</returns> + public static bool operator !=(ClaimsRequest one, ClaimsRequest other) { + return !(one == other); + } + + /// <summary> + /// Tests equality between two <see cref="ClaimsRequest"/> structs. + /// </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) { + ClaimsRequest other = obj as ClaimsRequest; + if (other == null) { + return false; + } + + return + this.BirthDate.Equals(other.BirthDate) && + this.Country.Equals(other.Country) && + this.Language.Equals(other.Language) && + this.Email.Equals(other.Email) && + this.FullName.Equals(other.FullName) && + this.Gender.Equals(other.Gender) && + this.Nickname.Equals(other.Nickname) && + this.PostalCode.Equals(other.PostalCode) && + this.TimeZone.Equals(other.TimeZone) && + this.PolicyUrl.EqualsNullSafe(other.PolicyUrl); + } + + /// <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() { + // It's important that if Equals returns true that the hash code also equals, + // so returning base.GetHashCode() is a BAD option. + // Return 1 is simple and poor for dictionary storage, but considering that every + // ClaimsRequest formulated at a single RP will likely have all the same fields, + // even a good hash code function will likely generate the same hash code. So + // we just cut to the chase and return a simple one. + return 1; + } + + /// <summary> + /// Renders the requested information as a string. + /// </summary> + /// <returns> + /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. + /// </returns> + public override string ToString() { + string format = @"Nickname = '{0}' +Email = '{1}' +FullName = '{2}' +Birthdate = '{3}' +Gender = '{4}' +PostalCode = '{5}' +Country = '{6}' +Language = '{7}' +TimeZone = '{8}'"; + return string.Format(CultureInfo.CurrentCulture, format, this.Nickname, this.Email, this.FullName, this.BirthDate, this.Gender, this.PostalCode, this.Country, this.Language, this.TimeZone); + } + + /// <summary> + /// Prepares a Simple Registration response extension that is compatible with the + /// version of Simple Registration used in the request message. + /// </summary> + /// <returns>The newly created <see cref="ClaimsResponse"/> instance.</returns> + public ClaimsResponse CreateResponse() { + if (this.typeUriDeserializedFrom == null) { + throw new InvalidOperationException(OpenIdStrings.CallDeserializeBeforeCreateResponse); + } + + return new ClaimsResponse(this.typeUriDeserializedFrom); + } + + /// <summary> + /// Sets the profile request properties according to a list of + /// field names that might have been passed in the OpenId query dictionary. + /// </summary> + /// <param name="fieldNames"> + /// The list of field names that should receive a given + /// <paramref name="requestLevel"/>. These field names should match + /// the OpenId specification for field names, omitting the 'openid.sreg' prefix. + /// </param> + /// <param name="requestLevel">The none/request/require state of the listed fields.</param> + internal void SetProfileRequestFromList(IEnumerable<string> fieldNames, DemandLevel requestLevel) { + foreach (string field in fieldNames) { + switch (field) { + case "": // this occurs for empty lists + break; + case Constants.nickname: + this.Nickname = requestLevel; + break; + case Constants.email: + this.Email = requestLevel; + break; + case Constants.fullname: + this.FullName = requestLevel; + break; + case Constants.dob: + this.BirthDate = requestLevel; + break; + case Constants.gender: + this.Gender = requestLevel; + break; + case Constants.postcode: + this.PostalCode = requestLevel; + break; + case Constants.country: + this.Country = requestLevel; + break; + case Constants.language: + this.Language = requestLevel; + break; + case Constants.timezone: + this.TimeZone = requestLevel; + break; + default: + Logger.OpenId.WarnFormat("ClaimsRequest.SetProfileRequestFromList: Unrecognized field name '{0}'.", field); + break; + } + } + } + + /// <summary> + /// Assembles the profile parameter names that have a given <see cref="DemandLevel"/>. + /// </summary> + /// <param name="level">The demand level (request, require, none).</param> + /// <returns>An array of the profile parameter names that meet the criteria.</returns> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")] + private string[] AssembleProfileFields(DemandLevel level) { + List<string> fields = new List<string>(10); + if (this.Nickname == level) { + fields.Add(Constants.nickname); + } if (this.Email == level) { + fields.Add(Constants.email); + } if (this.FullName == level) { + fields.Add(Constants.fullname); + } if (this.BirthDate == level) { + fields.Add(Constants.dob); + } if (this.Gender == level) { + fields.Add(Constants.gender); + } if (this.PostalCode == level) { + fields.Add(Constants.postcode); + } if (this.Country == level) { + fields.Add(Constants.country); + } if (this.Language == level) { + fields.Add(Constants.language); + } if (this.TimeZone == level) { + fields.Add(Constants.timezone); + } + + return fields.ToArray(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs new file mode 100644 index 0000000..d4df028 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs @@ -0,0 +1,357 @@ +//----------------------------------------------------------------------- +// <copyright file="ClaimsResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Net.Mail; + using System.Text; + using System.Text.RegularExpressions; + using System.Xml.Serialization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A struct storing Simple Registration field values describing an + /// authenticating user. + /// </summary> + [Serializable] + public sealed class ClaimsResponse : ExtensionBase, IClientScriptExtensionResponse, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if ((typeUri == Constants.sreg_ns || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) { + return new ClaimsResponse(typeUri); + } + + return null; + }; + + /// <summary> + /// The allowed format for birthdates. + /// </summary> + private static readonly Regex birthDateValidator = new Regex(@"^\d\d\d\d-\d\d-\d\d$"); + + /// <summary> + /// Storage for the raw string birthdate value. + /// </summary> + private string birthDateRaw; + + /// <summary> + /// Backing field for the <see cref="BirthDate"/> property. + /// </summary> + private DateTime? birthDate; + + /// <summary> + /// Backing field for the <see cref="Culture"/> property. + /// </summary> + private CultureInfo culture; + + /// <summary> + /// Initializes a new instance of the <see cref="ClaimsResponse"/> class. + /// </summary> + internal ClaimsResponse() + : this(Constants.sreg_ns) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ClaimsResponse"/> class. + /// </summary> + /// <param name="typeUriToUse"> + /// The type URI that must be used to identify this extension in the response message. + /// This value should be the same one the relying party used to send the extension request. + /// </param> + internal ClaimsResponse(string typeUriToUse) + : base(new Version(1, 0), typeUriToUse, Constants.AdditionalTypeUris) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUriToUse)); + } + + /// <summary> + /// Gets or sets the nickname the user goes by. + /// </summary> + [MessagePart(Constants.nickname)] + public string Nickname { get; set; } + + /// <summary> + /// Gets or sets the user's email address. + /// </summary> + [MessagePart(Constants.email)] + public string Email { get; set; } + + /// <summary> + /// Gets or sets the full name of a user as a single string. + /// </summary> + [MessagePart(Constants.fullname)] + public string FullName { get; set; } + + /// <summary> + /// Gets or sets the user's birthdate. + /// </summary> + public DateTime? BirthDate { + get { + return this.birthDate; + } + + set { + this.birthDate = value; + + // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors. + if (value.HasValue) { + this.birthDateRaw = value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } else { + this.birthDateRaw = null; + } + } + } + + /// <summary> + /// Gets or sets the raw birth date string given by the extension. + /// </summary> + /// <value>A string in the format yyyy-MM-dd.</value> + [MessagePart(Constants.dob)] + public string BirthDateRaw { + get { + return this.birthDateRaw; + } + + set { + ErrorUtilities.VerifyArgument(value == null || birthDateValidator.IsMatch(value), OpenIdStrings.SregInvalidBirthdate); + if (value != null) { + // Update the BirthDate property, if possible. + // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors. + // Some valid sreg dob values like "2000-00-00" will not work as a DateTime struct, + // in which case we null it out, but don't show any error. + DateTime newBirthDate; + if (DateTime.TryParse(value, out newBirthDate)) { + this.birthDate = newBirthDate; + } else { + Logger.OpenId.WarnFormat("Simple Registration birthdate '{0}' could not be parsed into a DateTime and may not include month and/or day information. Setting BirthDate property to null.", value); + this.birthDate = null; + } + } else { + this.birthDate = null; + } + + this.birthDateRaw = value; + } + } + + /// <summary> + /// Gets or sets the gender of the user. + /// </summary> + [MessagePart(Constants.gender, Encoder = typeof(GenderEncoder))] + public Gender? Gender { get; set; } + + /// <summary> + /// Gets or sets the zip code / postal code of the user. + /// </summary> + [MessagePart(Constants.postcode)] + public string PostalCode { get; set; } + + /// <summary> + /// Gets or sets the country of the user. + /// </summary> + [MessagePart(Constants.country)] + public string Country { get; set; } + + /// <summary> + /// Gets or sets the primary/preferred language of the user. + /// </summary> + [MessagePart(Constants.language)] + public string Language { get; set; } + + /// <summary> + /// Gets or sets the user's timezone. + /// </summary> + [MessagePart(Constants.timezone)] + public string TimeZone { get; set; } + + /// <summary> + /// Gets a combination of the user's full name and email address. + /// </summary> + public MailAddress MailAddress { + get { + if (string.IsNullOrEmpty(this.Email)) { + return null; + } else if (string.IsNullOrEmpty(this.FullName)) { + return new MailAddress(this.Email); + } else { + return new MailAddress(this.Email, this.FullName); + } + } + } + + /// <summary> + /// Gets or sets a combination o the language and country of the user. + /// </summary> + [XmlIgnore] + public CultureInfo Culture { + get { + if (this.culture == null && !string.IsNullOrEmpty(this.Language)) { + string cultureString = string.Empty; + cultureString = this.Language; + if (!string.IsNullOrEmpty(this.Country)) { + cultureString += "-" + this.Country; + } + this.culture = CultureInfo.GetCultureInfo(cultureString); + } + + return this.culture; + } + + set { + this.culture = value; + this.Language = (value != null) ? value.TwoLetterISOLanguageName : null; + int indexOfHyphen = (value != null) ? value.Name.IndexOf('-') : -1; + this.Country = indexOfHyphen > 0 ? value.Name.Substring(indexOfHyphen + 1) : null; + } + } + + /// <summary> + /// Gets a value indicating whether this extension is signed by the Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>. + /// </value> + public bool IsSignedByProvider { + get { return this.IsSignedByRemoteParty; } + } + + /// <summary> + /// Tests equality of two <see cref="ClaimsResponse"/> objects. + /// </summary> + /// <param name="one">One instance to compare.</param> + /// <param name="other">Another instance to compare.</param> + /// <returns>The result of the operator.</returns> + public static bool operator ==(ClaimsResponse one, ClaimsResponse other) { + return one.EqualsNullSafe(other); + } + + /// <summary> + /// Tests inequality of two <see cref="ClaimsResponse"/> objects. + /// </summary> + /// <param name="one">One instance to compare.</param> + /// <param name="other">Another instance to compare.</param> + /// <returns>The result of the operator.</returns> + public static bool operator !=(ClaimsResponse one, ClaimsResponse other) { + return !(one == other); + } + + /// <summary> + /// Tests equality of two <see cref="ClaimsResponse"/> objects. + /// </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) { + ClaimsResponse other = obj as ClaimsResponse; + if (other == null) { + return false; + } + + return + this.BirthDateRaw.EqualsNullSafe(other.BirthDateRaw) && + this.Country.EqualsNullSafe(other.Country) && + this.Language.EqualsNullSafe(other.Language) && + this.Email.EqualsNullSafe(other.Email) && + this.FullName.EqualsNullSafe(other.FullName) && + this.Gender.Equals(other.Gender) && + this.Nickname.EqualsNullSafe(other.Nickname) && + this.PostalCode.EqualsNullSafe(other.PostalCode) && + this.TimeZone.EqualsNullSafe(other.TimeZone); + } + + /// <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.Nickname != null) ? this.Nickname.GetHashCode() : base.GetHashCode(); + } + + #region IClientScriptExtension Members + + /// <summary> + /// Reads the extension information on an authentication response from the provider. + /// </summary> + /// <param name="response">The incoming OpenID response carrying the extension.</param> + /// <returns> + /// A Javascript snippet that when executed on the user agent returns an object with + /// the information deserialized from the extension response. + /// </returns> + /// <remarks> + /// This method is called <b>before</b> the signature on the assertion response has been + /// verified. Therefore all information in these fields should be assumed unreliable + /// and potentially falsified. + /// </remarks> + string IClientScriptExtensionResponse.InitializeJavaScriptData(IProtocolMessageWithExtensions response) { + var sreg = new Dictionary<string, string>(15); + + // Although we could probably whip up a trip with MessageDictionary + // to avoid explicitly setting each field, doing so would likely + // open ourselves up to security exploits from the OP as it would + // make possible sending arbitrary javascript in arbitrary field names. + sreg[Constants.nickname] = this.Nickname; + sreg[Constants.email] = this.Email; + sreg[Constants.fullname] = this.FullName; + sreg[Constants.dob] = this.BirthDateRaw; + sreg[Constants.gender] = this.Gender.HasValue ? this.Gender.Value.ToString() : null; + sreg[Constants.postcode] = this.PostalCode; + sreg[Constants.country] = this.Country; + sreg[Constants.language] = this.Language; + sreg[Constants.timezone] = this.TimeZone; + + return MessagingUtilities.CreateJsonObject(sreg, false); + } + + #endregion + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + // Null out empty values so we don't send out a lot of empty parameters. + this.Country = EmptyToNull(this.Country); + this.Email = EmptyToNull(this.Email); + this.FullName = EmptyToNull(this.FullName); + this.Language = EmptyToNull(this.Language); + this.Nickname = EmptyToNull(this.Nickname); + this.PostalCode = EmptyToNull(this.PostalCode); + this.TimeZone = EmptyToNull(this.TimeZone); + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + } + + #endregion + + /// <summary> + /// Translates an empty string value to null, or passes through non-empty values. + /// </summary> + /// <param name="value">The value to consider changing to null.</param> + /// <returns>Either null or a non-empty string.</returns> + private static string EmptyToNull(string value) { + return string.IsNullOrEmpty(value) ? null : value; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs new file mode 100644 index 0000000..9e00137 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs @@ -0,0 +1,46 @@ +// <auto-generated/> // disable StyleCop on this file +//----------------------------------------------------------------------- +// <copyright file="Constants.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { + using System; + using System.Collections.Generic; + using System.Text; + + /// <summary> + /// Simple Registration constants + /// </summary> + internal static class Constants { + internal const string sreg_ns = "http://openid.net/extensions/sreg/1.1"; + internal const string sreg_ns10 = "http://openid.net/sreg/1.0"; + internal const string sreg_ns11other = "http://openid.net/sreg/1.1"; + internal const string sreg_compatibility_alias = "sreg"; + internal const string policy_url = "policy_url"; + internal const string optional = "optional"; + internal const string required = "required"; + internal const string nickname = "nickname"; + internal const string email = "email"; + internal const string fullname = "fullname"; + internal const string dob = "dob"; + internal const string gender = "gender"; + internal const string postcode = "postcode"; + internal const string country = "country"; + internal const string language = "language"; + internal const string timezone = "timezone"; + internal static class Genders { + internal const string Male = "M"; + internal const string Female = "F"; + } + + /// <summary> + /// Additional type URIs that this extension is sometimes known by remote parties. + /// </summary> + internal static readonly string[] AdditionalTypeUris = new string[] { + Constants.sreg_ns10, + Constants.sreg_ns11other, + }; + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs new file mode 100644 index 0000000..7129270 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/DemandLevel.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// <copyright file="DemandLevel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { + /// <summary> + /// Specifies what level of interest a relying party has in obtaining the value + /// of a given field offered by the Simple Registration extension. + /// </summary> + public enum DemandLevel { + /// <summary> + /// The relying party has no interest in obtaining this field. + /// </summary> + NoRequest, + + /// <summary> + /// The relying party would like the value of this field, but wants + /// the Provider to display the field to the user as optionally provided. + /// </summary> + Request, + + /// <summary> + /// The relying party considers this a required field as part of + /// authentication. The Provider and/or user agent MAY still choose to + /// not provide the value of the field however, according to the + /// Simple Registration extension specification. + /// </summary> + Require, + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs new file mode 100644 index 0000000..979c481 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Gender.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="Gender.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// Indicates the gender of a user. + /// </summary> + public enum Gender { + /// <summary> + /// The user is male. + /// </summary> + Male, + + /// <summary> + /// The user is female. + /// </summary> + Female, + } + + /// <summary> + /// Encodes/decodes the Simple Registration Gender type to its string representation. + /// </summary> + internal class GenderEncoder : IMessagePartEncoder { + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + var gender = (Gender?)value; + if (gender.HasValue) { + switch (gender.Value) { + case Gender.Male: return Constants.Genders.Male; + case Gender.Female: return Constants.Genders.Female; + } + } + + return null; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + switch (value) { + case Constants.Genders.Male: return SimpleRegistration.Gender.Male; + case Constants.Genders.Female: return SimpleRegistration.Gender.Female; + default: throw new FormatException(); + } + } + + #endregion + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs new file mode 100644 index 0000000..1dcda27 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/StandardOpenIdExtensionFactory.cs @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardOpenIdExtensionFactory.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions { + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + using DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy; + using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration; + using DotNetOpenAuth.OpenId.Extensions.UI; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// An OpenID extension factory that supports registration so that third-party + /// extensions can add themselves to this library's supported extension list. + /// </summary> + internal class StandardOpenIdExtensionFactory : IOpenIdExtensionFactory { + /// <summary> + /// A collection of the registered OpenID extensions. + /// </summary> + private List<CreateDelegate> registeredExtensions = new List<CreateDelegate>(); + + /// <summary> + /// Initializes a new instance of the <see cref="StandardOpenIdExtensionFactory"/> class. + /// </summary> + internal StandardOpenIdExtensionFactory() { + this.RegisterExtension(ClaimsRequest.Factory); + this.RegisterExtension(ClaimsResponse.Factory); + this.RegisterExtension(FetchRequest.Factory); + this.RegisterExtension(FetchResponse.Factory); + this.RegisterExtension(StoreRequest.Factory); + this.RegisterExtension(StoreResponse.Factory); + this.RegisterExtension(PolicyRequest.Factory); + this.RegisterExtension(PolicyResponse.Factory); + this.RegisterExtension(AuthorizationRequest.Factory); + this.RegisterExtension(AuthorizationApprovedResponse.Factory); + this.RegisterExtension(AuthorizationDeclinedResponse.Factory); + this.RegisterExtension(UIRequest.Factory); + } + + /// <summary> + /// A delegate that individual extensions may register with this factory. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + internal delegate IOpenIdMessageExtension CreateDelegate(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole); + + #region IOpenIdExtensionFactory Members + + /// <summary> + /// Creates a new instance of some extension based on the received extension parameters. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + /// <remarks> + /// This factory method need only initialize properties in the instantiated extension object + /// that are not bound using <see cref="MessagePartAttribute"/>. + /// </remarks> + public IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole) { + foreach (var factoryMethod in this.registeredExtensions) { + IOpenIdMessageExtension result = factoryMethod(typeUri, data, baseMessage, isProviderRole); + if (result != null) { + return result; + } + } + + return null; + } + + #endregion + + /// <summary> + /// Registers a new extension delegate. + /// </summary> + /// <param name="creator">The factory method that can create the extension.</param> + internal void RegisterExtension(CreateDelegate creator) { + this.registeredExtensions.Add(creator); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs new file mode 100644 index 0000000..1cc920a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIConstants.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="UIConstants.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.UI { + /// <summary> + /// Constants used to support the UI extension. + /// </summary> + internal static class UIConstants { + /// <summary> + /// The type URI associated with this extension. + /// </summary> + internal const string UITypeUri = "http://specs.openid.net/extensions/ui/1.0"; + + /// <summary> + /// The Type URI that appears in an XRDS document when the OP supports popups through the UI extension. + /// </summary> + internal const string PopupSupported = "http://specs.openid.net/extensions/ui/1.0/mode/popup"; + + /// <summary> + /// The Type URI that appears in an XRDS document when the OP supports the RP + /// specifying the user's preferred language through the UI extension. + /// </summary> + internal const string LangPrefSupported = "http://specs.openid.net/extensions/ui/1.0/lang-pref"; + + /// <summary> + /// The Type URI that appears in the XRDS document when the OP supports the RP + /// specifying the icon for the OP to display during authentication through the UI extension. + /// </summary> + internal const string IconSupported = "http://specs.openid.net/extensions/ui/1.0/icon"; + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs new file mode 100644 index 0000000..8e3e20f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIModes.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------- +// <copyright file="UIModes.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.UI { + /// <summary> + /// Valid values for the <c>mode</c> parameter of the OpenID User Interface extension. + /// </summary> + public static class UIModes { + /// <summary> + /// Indicates that the Provider's authentication page appears in a popup window. + /// </summary> + /// <value>The constant <c>"popup"</c>.</value> + /// <remarks> + /// <para>The RP SHOULD create the popup to be 450 pixels wide and 500 pixels tall. The popup MUST have the address bar displayed, and MUST be in a standalone browser window. The contents of the popup MUST NOT be framed by the RP. </para> + /// <para>The RP SHOULD open the popup centered above the main browser window, and SHOULD dim the contents of the parent window while the popup is active. The RP SHOULD ensure that the user is not surprised by the appearance of the popup, and understands how to interact with it. </para> + /// <para>To keep the user popup user experience consistent, it is RECOMMENDED that the OP does not resize the popup window unless the OP requires additional space to show special features that are not usually displayed as part of the default popup user experience. </para> + /// <para>The OP MAY close the popup without returning a response to the RP. Closing the popup without sending a response should be interpreted as a negative assertion. </para> + /// <para>The response to an authentication request in a popup is unchanged from [OpenID 2.0] (OpenID 2.0 Workgroup, “OpenID 2.0,” .). Relying Parties detecting that the popup was closed without receiving an authentication response SHOULD interpret the close event to be a negative assertion. </para> + /// </remarks> + public const string Popup = "popup"; + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs new file mode 100644 index 0000000..df36b5e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIRequest.cs @@ -0,0 +1,261 @@ +//----------------------------------------------------------------------- +// <copyright file="UIRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.UI { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + using DotNetOpenAuth.Xrds; + + /// <summary> + /// OpenID User Interface extension 1.0 request message. + /// </summary> + /// <remarks> + /// <para>Implements the extension described by: http://wiki.openid.net/f/openid_ui_extension_draft01.html </para> + /// <para>This extension only applies to checkid_setup requests, since checkid_immediate requests display + /// no UI to the user. </para> + /// <para>For rules about how the popup window should be displayed, please see the documentation of + /// <see cref="UIModes.Popup"/>. </para> + /// <para>An RP may determine whether an arbitrary OP supports this extension (and thereby determine + /// whether to use a standard full window redirect or a popup) via the + /// <see cref="IdentifierDiscoveryResult.IsExtensionSupported<T>()"/> method.</para> + /// </remarks> + [Serializable] + public class UIRequest : IOpenIdMessageExtension, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if (typeUri == UIConstants.UITypeUri && isProviderRole) { + return new UIRequest(); + } + + return null; + }; + + /// <summary> + /// Additional type URIs that this extension is sometimes known by remote parties. + /// </summary> + private static readonly string[] additionalTypeUris = new string[] { + UIConstants.LangPrefSupported, + UIConstants.PopupSupported, + UIConstants.IconSupported, + }; + + /// <summary> + /// Backing store for <see cref="ExtraData"/>. + /// </summary> + private Dictionary<string, string> extraData = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="UIRequest"/> class. + /// </summary> + public UIRequest() { + this.LanguagePreference = new[] { CultureInfo.CurrentUICulture }; + this.Mode = UIModes.Popup; + } + + /// <summary> + /// Gets or sets the list of user's preferred languages, sorted in decreasing preferred order. + /// </summary> + /// <value>The default is the <see cref="CultureInfo.CurrentUICulture"/> of the thread that created this instance.</value> + /// <remarks> + /// The user's preferred languages as a [BCP 47] language priority list, represented as a comma-separated list of BCP 47 basic language ranges in descending priority order. For instance, the value "fr-CA,fr-FR,en-CA" represents the preference for French spoken in Canada, French spoken in France, followed by English spoken in Canada. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "By design.")] + [MessagePart("lang", AllowEmpty = false)] + public CultureInfo[] LanguagePreference { get; set; } + + /// <summary> + /// Gets or sets the style of UI that the RP is hosting the OP's authentication page in. + /// </summary> + /// <value>Some value from the <see cref="UIModes"/> class. Defaults to <see cref="UIModes.Popup"/>.</value> + [MessagePart("mode", AllowEmpty = false, IsRequired = true)] + public string Mode { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the Relying Party has an icon + /// it would like the Provider to display to the user while asking them + /// whether they would like to log in. + /// </summary> + /// <value><c>true</c> if the Provider should display an icon; otherwise, <c>false</c>.</value> + /// <remarks> + /// By default, the Provider displays the relying party's favicon.ico. + /// </remarks> + [MessagePart("icon", AllowEmpty = false, IsRequired = false)] + public bool? Icon { get; set; } + + #region IOpenIdMessageExtension Members + + /// <summary> + /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements. + /// </summary> + /// <value></value> + public string TypeUri { get { return UIConstants.UITypeUri; } } + + /// <summary> + /// Gets the additional TypeURIs that are supported by this extension, in preferred order. + /// May be empty if none other than <see cref="TypeUri"/> is supported, but + /// should not be null. + /// </summary> + /// <remarks> + /// Useful for reading in messages with an older version of an extension. + /// The value in the <see cref="TypeUri"/> property is always checked before + /// trying this list. + /// If you do support multiple versions of an extension using this method, + /// consider adding a CreateResponse method to your request extension class + /// so that the response can have the context it needs to remain compatible + /// given the version of the extension in the request message. + /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example. + /// </remarks> + public IEnumerable<string> AdditionalSupportedTypeUris { get { return additionalTypeUris; } } + + /// <summary> + /// Gets or sets a value indicating whether this extension was + /// signed by the sender. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the sender; otherwise, <c>false</c>. + /// </value> + public bool IsSignedByRemoteParty { get; set; } + + #endregion + + #region IMessage Properties + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + /// <value>The value 1.0.</value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + public Version Version { + get { return new Version(1, 0); } + } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + public IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + #endregion + + /// <summary> + /// Gets the URL of the RP icon for the OP to display. + /// </summary> + /// <param name="realm">The realm of the RP where the authentication request originated.</param> + /// <param name="webRequestHandler">The web request handler to use for discovery. + /// Usually available via <see cref="Channel.WebRequestHandler">OpenIdProvider.Channel.WebRequestHandler</see>.</param> + /// <returns> + /// A sequence of the RP's icons it has available for the Provider to display, in decreasing preferred order. + /// </returns> + /// <value>The icon URL.</value> + /// <remarks> + /// This property is automatically set for the OP with the result of RP discovery. + /// RPs should set this value by including an entry such as this in their XRDS document. + /// <example> + /// <Service xmlns="xri://$xrd*($v*2.0)"> + /// <Type>http://specs.openid.net/extensions/ui/icon</Type> + /// <URI>http://consumer.example.com/images/image.jpg</URI> + /// </Service> + /// </example> + /// </remarks> + public static IEnumerable<Uri> GetRelyingPartyIconUrls(Realm realm, IDirectWebRequestHandler webRequestHandler) { + Contract.Requires(realm != null); + Contract.Requires(webRequestHandler != null); + ErrorUtilities.VerifyArgumentNotNull(realm, "realm"); + ErrorUtilities.VerifyArgumentNotNull(webRequestHandler, "webRequestHandler"); + + XrdsDocument xrds = realm.Discover(webRequestHandler, false); + if (xrds == null) { + return Enumerable.Empty<Uri>(); + } else { + return xrds.FindRelyingPartyIcons(); + } + } + + /// <summary> + /// Gets the URL of the RP icon for the OP to display. + /// </summary> + /// <param name="realm">The realm of the RP where the authentication request originated.</param> + /// <param name="provider">The Provider instance used to obtain the authentication request.</param> + /// <returns> + /// A sequence of the RP's icons it has available for the Provider to display, in decreasing preferred order. + /// </returns> + /// <value>The icon URL.</value> + /// <remarks> + /// This property is automatically set for the OP with the result of RP discovery. + /// RPs should set this value by including an entry such as this in their XRDS document. + /// <example> + /// <Service xmlns="xri://$xrd*($v*2.0)"> + /// <Type>http://specs.openid.net/extensions/ui/icon</Type> + /// <URI>http://consumer.example.com/images/image.jpg</URI> + /// </Service> + /// </example> + /// </remarks> + public static IEnumerable<Uri> GetRelyingPartyIconUrls(Realm realm, OpenIdProvider provider) { + Contract.Requires(realm != null); + Contract.Requires(provider != null); + ErrorUtilities.VerifyArgumentNotNull(realm, "realm"); + ErrorUtilities.VerifyArgumentNotNull(provider, "provider"); + + return GetRelyingPartyIconUrls(realm, provider.Channel.WebRequestHandler); + } + + #region IMessage methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public void EnsureValidMessage() { + } + + #endregion + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + public void OnSending() { + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + public void OnReceiving() { + if (this.LanguagePreference != null) { + // TODO: see if we can change the CultureInfo.CurrentUICulture somehow + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/UI/UIUtilities.cs new file mode 100644 index 0000000..cee6882 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/HmacShaAssociation.cs b/src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs new file mode 100644 index 0000000..29da1be --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs @@ -0,0 +1,306 @@ +//----------------------------------------------------------------------- +// <copyright file="HmacShaAssociation.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Security.Cryptography; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// An association that uses the HMAC-SHA family of algorithms for message signing. + /// </summary> + [ContractVerification(true)] + internal class HmacShaAssociation : Association { + /// <summary> + /// The default lifetime of a shared association when no lifetime is given + /// for a specific association type. + /// </summary> + private static readonly TimeSpan DefaultMaximumLifetime = TimeSpan.FromDays(14); + + /// <summary> + /// A list of HMAC-SHA algorithms in order of decreasing bit lengths. + /// </summary> + private static HmacSha[] hmacShaAssociationTypes = new List<HmacSha> { + new HmacSha { + CreateHasher = secretKey => new HMACSHA512(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA512, + BaseHashAlgorithm = new SHA512Managed(), + }, + new HmacSha { + CreateHasher = secretKey => new HMACSHA384(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA384, + BaseHashAlgorithm = new SHA384Managed(), + }, + new HmacSha { + CreateHasher = secretKey => new HMACSHA256(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA256, + BaseHashAlgorithm = new SHA256Managed(), + }, + new HmacSha { + CreateHasher = secretKey => new HMACSHA1(secretKey), + GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA1, + BaseHashAlgorithm = new SHA1Managed(), + }, + } .ToArray(); + + /// <summary> + /// The specific variety of HMAC-SHA this association is based on (whether it be HMAC-SHA1, HMAC-SHA256, etc.) + /// </summary> + private HmacSha typeIdentity; + + /// <summary> + /// Initializes a new instance of the <see cref="HmacShaAssociation"/> class. + /// </summary> + /// <param name="typeIdentity">The specific variety of HMAC-SHA this association is based on (whether it be HMAC-SHA1, HMAC-SHA256, etc.)</param> + /// <param name="handle">The association handle.</param> + /// <param name="secret">The association secret.</param> + /// <param name="totalLifeLength">The time duration the association will be good for.</param> + private HmacShaAssociation(HmacSha typeIdentity, string handle, byte[] secret, TimeSpan totalLifeLength) + : base(handle, secret, totalLifeLength, DateTime.UtcNow) { + Contract.Requires<ArgumentNullException>(typeIdentity != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentNullException>(secret != null); + Contract.Requires<ArgumentOutOfRangeException>(totalLifeLength > TimeSpan.Zero); + Contract.Ensures(this.TotalLifeLength == totalLifeLength); + ErrorUtilities.VerifyProtocol(secret.Length == typeIdentity.SecretLength, OpenIdStrings.AssociationSecretAndTypeLengthMismatch, secret.Length, typeIdentity.GetAssociationType(Protocol.Default)); + + this.typeIdentity = typeIdentity; + } + + /// <summary> + /// Gets the length (in bits) of the hash this association creates when signing. + /// </summary> + public override int HashBitLength { + get { + Protocol protocol = Protocol.Default; + return HmacShaAssociation.GetSecretLength(protocol, this.GetAssociationType(protocol)) * 8; + } + } + + /// <summary> + /// Creates an HMAC-SHA association. + /// </summary> + /// <param name="protocol">The OpenID protocol version that the request for an association came in on.</param> + /// <param name="associationType">The value of the openid.assoc_type parameter.</param> + /// <param name="handle">The association handle.</param> + /// <param name="secret">The association secret.</param> + /// <param name="totalLifeLength">How long the association will be good for.</param> + /// <returns>The newly created association.</returns> + public static HmacShaAssociation Create(Protocol protocol, string associationType, string handle, byte[] secret, TimeSpan totalLifeLength) { + Contract.Requires<ArgumentNullException>(protocol != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(associationType)); + Contract.Requires<ArgumentNullException>(secret != null); + Contract.Ensures(Contract.Result<HmacShaAssociation>() != null); + HmacSha match = hmacShaAssociationTypes.FirstOrDefault(sha => String.Equals(sha.GetAssociationType(protocol), associationType, StringComparison.Ordinal)); + ErrorUtilities.VerifyProtocol(match != null, OpenIdStrings.NoAssociationTypeFoundByName, associationType); + return new HmacShaAssociation(match, handle, secret, totalLifeLength); + } + + /// <summary> + /// Creates an association with the specified handle, secret, and lifetime. + /// </summary> + /// <param name="handle">The handle.</param> + /// <param name="secret">The secret.</param> + /// <param name="totalLifeLength">Total lifetime.</param> + /// <returns>The newly created association.</returns> + public static HmacShaAssociation Create(string handle, byte[] secret, TimeSpan totalLifeLength) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle)); + Contract.Requires<ArgumentNullException>(secret != null); + Contract.Ensures(Contract.Result<HmacShaAssociation>() != null); + + HmacSha shaType = hmacShaAssociationTypes.FirstOrDefault(sha => sha.SecretLength == secret.Length); + ErrorUtilities.VerifyProtocol(shaType != null, OpenIdStrings.NoAssociationTypeFoundByLength, secret.Length); + return new HmacShaAssociation(shaType, handle, secret, totalLifeLength); + } + + /// <summary> + /// Returns the length of the shared secret (in bytes). + /// </summary> + /// <param name="protocol">The protocol version being used that will be used to lookup the text in <paramref name="associationType"/></param> + /// <param name="associationType">The value of the protocol argument specifying the type of association. For example: "HMAC-SHA1".</param> + /// <returns>The length (in bytes) of the association secret.</returns> + /// <exception cref="ProtocolException">Thrown if no association can be found by the given name.</exception> + public static int GetSecretLength(Protocol protocol, string associationType) { + HmacSha match = hmacShaAssociationTypes.FirstOrDefault(shaType => String.Equals(shaType.GetAssociationType(protocol), associationType, StringComparison.Ordinal)); + ErrorUtilities.VerifyProtocol(match != null, OpenIdStrings.NoAssociationTypeFoundByName, associationType); + return match.SecretLength; + } + + /// <summary> + /// Creates a new association of a given type at an OpenID Provider. + /// </summary> + /// <param name="protocol">The protocol.</param> + /// <param name="associationType">Type of the association (i.e. HMAC-SHA1 or HMAC-SHA256)</param> + /// <param name="associationUse">A value indicating whether the new association will be used privately by the Provider for "dumb mode" authentication + /// or shared with the Relying Party for "smart mode" authentication.</param> + /// <param name="associationStore">The Provider's association store.</param> + /// <param name="securitySettings">The security settings of the Provider.</param> + /// <returns> + /// The newly created association. + /// </returns> + /// <remarks> + /// The new association is NOT automatically put into an association store. This must be done by the caller. + /// </remarks> + internal static HmacShaAssociation Create(Protocol protocol, string associationType, AssociationRelyingPartyType associationUse, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(protocol != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(associationType)); + Contract.Requires<ArgumentNullException>(associationStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + Contract.Ensures(Contract.Result<HmacShaAssociation>() != null); + + int secretLength = GetSecretLength(protocol, associationType); + + // Generate the secret that will be used for signing + byte[] secret = MessagingUtilities.GetCryptoRandomData(secretLength); + + TimeSpan lifetime; + if (associationUse == AssociationRelyingPartyType.Smart) { + if (!securitySettings.AssociationLifetimes.TryGetValue(associationType, out lifetime)) { + lifetime = DefaultMaximumLifetime; + } + } else { + lifetime = DumbSecretLifetime; + } + + string handle = associationStore.Serialize(secret, DateTime.UtcNow + lifetime, associationUse == AssociationRelyingPartyType.Dumb); + + Contract.Assert(protocol != null); // All the way up to the method call, the condition holds, yet we get a Requires failure next + Contract.Assert(secret != null); + Contract.Assert(!String.IsNullOrEmpty(associationType)); + var result = Create(protocol, associationType, handle, secret, lifetime); + return result; + } + + /// <summary> + /// Looks for the first association type in a preferred-order list that is + /// likely to be supported given a specific OpenID version and the security settings, + /// and perhaps a matching Diffie-Hellman session type. + /// </summary> + /// <param name="protocol">The OpenID version that dictates which associations are available.</param> + /// <param name="highSecurityIsBetter">A value indicating whether to consider higher strength security to be better. Use <c>true</c> for initial association requests from the Relying Party; use <c>false</c> from Providers when the Relying Party asks for an unrecognized association in order to pick a suggested alternative that is likely to be supported on both sides.</param> + /// <param name="securityRequirements">The set of requirements the selected association type must comply to.</param> + /// <param name="requireMatchingDHSessionType">Use <c>true</c> for HTTP associations, <c>false</c> for HTTPS associations.</param> + /// <param name="associationType">The resulting association type's well known protocol name. (i.e. HMAC-SHA256)</param> + /// <param name="sessionType">The resulting session type's well known protocol name, if a matching one is available. (i.e. DH-SHA256)</param> + /// <returns> + /// True if a qualifying association could be found; false otherwise. + /// </returns> + internal static bool TryFindBestAssociation(Protocol protocol, bool highSecurityIsBetter, SecuritySettings securityRequirements, bool requireMatchingDHSessionType, out string associationType, out string sessionType) { + Contract.Requires<ArgumentNullException>(protocol != null); + Contract.Requires<ArgumentNullException>(securityRequirements != null); + + associationType = null; + sessionType = null; + + // We use AsEnumerable() to avoid VerificationException (http://stackoverflow.com/questions/478422/why-does-simple-array-and-linq-generate-verificationexception-operation-could-de) + IEnumerable<HmacSha> preferredOrder = highSecurityIsBetter ? + hmacShaAssociationTypes.AsEnumerable() : hmacShaAssociationTypes.Reverse(); + + foreach (HmacSha sha in preferredOrder) { + int hashSizeInBits = sha.SecretLength * 8; + if (hashSizeInBits > securityRequirements.MaximumHashBitLength || + hashSizeInBits < securityRequirements.MinimumHashBitLength) { + continue; + } + sessionType = DiffieHellmanUtilities.GetNameForSize(protocol, hashSizeInBits); + if (requireMatchingDHSessionType && sessionType == null) { + continue; + } + associationType = sha.GetAssociationType(protocol); + if (associationType == null) { + continue; + } + + return true; + } + + return false; + } + + /// <summary> + /// Determines whether a named Diffie-Hellman session type and association type can be used together. + /// </summary> + /// <param name="protocol">The protocol carrying the names of the session and association types.</param> + /// <param name="associationType">The value of the openid.assoc_type parameter.</param> + /// <param name="sessionType">The value of the openid.session_type parameter.</param> + /// <returns> + /// <c>true</c> if the named association and session types are compatible; otherwise, <c>false</c>. + /// </returns> + internal static bool IsDHSessionCompatible(Protocol protocol, string associationType, string sessionType) { + Contract.Requires<ArgumentNullException>(protocol != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(associationType)); + Contract.Requires<ArgumentNullException>(sessionType != null); + + // All association types can work when no DH session is used at all. + if (string.Equals(sessionType, protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal)) { + return true; + } + + // When there _is_ a DH session, it must match in hash length with the association type. + int associationSecretLengthInBytes = GetSecretLength(protocol, associationType); + int sessionHashLengthInBytes = DiffieHellmanUtilities.Lookup(protocol, sessionType).HashSize / 8; + return associationSecretLengthInBytes == sessionHashLengthInBytes; + } + + /// <summary> + /// Gets the string to pass as the assoc_type value in the OpenID protocol. + /// </summary> + /// <param name="protocol">The protocol version of the message that the assoc_type value will be included in.</param> + /// <returns> + /// The value that should be used for the openid.assoc_type parameter. + /// </returns> + [Pure] + internal override string GetAssociationType(Protocol protocol) { + return this.typeIdentity.GetAssociationType(protocol); + } + + /// <summary> + /// Returns the specific hash algorithm used for message signing. + /// </summary> + /// <returns> + /// The hash algorithm used for message signing. + /// </returns> + [Pure] + protected override HashAlgorithm CreateHasher() { + var result = this.typeIdentity.CreateHasher(SecretKey); + Contract.Assume(result != null); + return result; + } + + /// <summary> + /// Provides information about some HMAC-SHA hashing algorithm that OpenID supports. + /// </summary> + private class HmacSha { + /// <summary> + /// Gets or sets the function that takes a particular OpenID version and returns the value of the openid.assoc_type parameter in that protocol. + /// </summary> + internal Func<Protocol, string> GetAssociationType { get; set; } + + /// <summary> + /// Gets or sets a function that will create the <see cref="HashAlgorithm"/> using a given shared secret for the mac. + /// </summary> + internal Func<byte[], HashAlgorithm> CreateHasher { get; set; } + + /// <summary> + /// Gets or sets the base hash algorithm. + /// </summary> + internal HashAlgorithm BaseHashAlgorithm { get; set; } + + /// <summary> + /// Gets the size of the hash (in bytes). + /// </summary> + internal int SecretLength { get { return this.BaseHashAlgorithm.HashSize / 8; } } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth.OpenId/OpenId/HostMetaDiscoveryService.cs new file mode 100644 index 0000000..215ea24 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/IIdentifierDiscoveryService.cs b/src/DotNetOpenAuth.OpenId/OpenId/IIdentifierDiscoveryService.cs new file mode 100644 index 0000000..fcea327 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/IOpenIdApplicationStore.cs b/src/DotNetOpenAuth.OpenId/OpenId/IOpenIdApplicationStore.cs new file mode 100644 index 0000000..6c04a81 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/IOpenIdApplicationStore.cs @@ -0,0 +1,16 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdApplicationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// A hybrid of the store interfaces that an OpenID Provider must implement, and + /// an OpenID Relying Party may implement to operate in stateful (smart) mode. + /// </summary> + public interface IOpenIdApplicationStore : ICryptoKeyStore, INonceStore { + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Identifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/Identifier.cs new file mode 100644 index 0000000..305976a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Identifier.cs @@ -0,0 +1,298 @@ +//----------------------------------------------------------------------- +// <copyright file="Identifier.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An Identifier is either a "http" or "https" URI, or an XRI. + /// </summary> + [Serializable] + [ContractVerification(true)] + [Pure] + [ContractClass(typeof(IdentifierContract))] + public abstract class Identifier { + /// <summary> + /// Initializes a new instance of the <see cref="Identifier"/> class. + /// </summary> + /// <param name="originalString">The original string before any normalization.</param> + /// <param name="isDiscoverySecureEndToEnd">Whether the derived class is prepared to guarantee end-to-end discovery + /// and initial redirect for authentication is performed using SSL.</param> + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "string", Justification = "Emphasis on string instead of the strong-typed Identifier.")] + protected Identifier(string originalString, bool isDiscoverySecureEndToEnd) { + this.OriginalString = originalString; + this.IsDiscoverySecureEndToEnd = isDiscoverySecureEndToEnd; + } + + /// <summary> + /// Gets the original string that was normalized to create this Identifier. + /// </summary> + internal string OriginalString { get; private set; } + + /// <summary> + /// Gets the Identifier in the form in which it should be serialized. + /// </summary> + /// <value> + /// For Identifiers that were originally deserialized, this is the exact same + /// string that was deserialized. For Identifiers instantiated in some other way, this is + /// the normalized form of the string used to instantiate the identifier. + /// </value> + internal virtual string SerializedString { + get { return this.IsDeserializedInstance ? this.OriginalString : this.ToString(); } + } + + /// <summary> + /// Gets or sets a value indicating whether <see cref="Identifier"/> instances are considered equal + /// based solely on their string reprsentations. + /// </summary> + /// <remarks> + /// This property serves as a test hook, so that MockIdentifier instances can be considered "equal" + /// to UriIdentifier instances. + /// </remarks> + protected internal static bool EqualityOnStrings { get; set; } + + /// <summary> + /// Gets a value indicating whether this Identifier will ensure SSL is + /// used throughout the discovery phase and initial redirect of authentication. + /// </summary> + /// <remarks> + /// If this is <c>false</c>, a value of <c>true</c> may be obtained by calling + /// <see cref="TryRequireSsl"/>. + /// </remarks> + protected internal bool IsDiscoverySecureEndToEnd { get; private set; } + + /// <summary> + /// Gets a value indicating whether this instance was initialized from + /// deserializing a message. + /// </summary> + /// <remarks> + /// This is interesting because when an Identifier comes from the network, + /// we can't normalize it and then expect signatures to still verify. + /// But if the Identifier is initialized locally, we can and should normalize it + /// before serializing it. + /// </remarks> + protected bool IsDeserializedInstance { get; private set; } + + /// <summary> + /// Converts the string representation of an Identifier to its strong type. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <returns>The particular Identifier instance to represent the value given.</returns> + [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Not all identifiers are URIs.")] + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "Our named alternate is Parse.")] + [DebuggerStepThrough] + public static implicit operator Identifier(string identifier) { + Contract.Requires<ArgumentException>(identifier == null || identifier.Length > 0); + Contract.Ensures((identifier == null) == (Contract.Result<Identifier>() == null)); + + if (identifier == null) { + return null; + } + return Parse(identifier); + } + + /// <summary> + /// Converts a given Uri to a strongly-typed Identifier. + /// </summary> + /// <param name="identifier">The identifier to convert.</param> + /// <returns>The result of the conversion.</returns> + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "We have a Parse function.")] + [DebuggerStepThrough] + public static implicit operator Identifier(Uri identifier) { + Contract.Ensures((identifier == null) == (Contract.Result<Identifier>() == null)); + if (identifier == null) { + return null; + } + + return new UriIdentifier(identifier); + } + + /// <summary> + /// Converts an Identifier to its string representation. + /// </summary> + /// <param name="identifier">The identifier to convert to a string.</param> + /// <returns>The result of the conversion.</returns> + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "We have a Parse function.")] + [DebuggerStepThrough] + public static implicit operator string(Identifier identifier) { + Contract.Ensures((identifier == null) == (Contract.Result<string>() == null)); + if (identifier == null) { + return null; + } + return identifier.ToString(); + } + + /// <summary> + /// Parses an identifier string and automatically determines + /// whether it is an XRI or URI. + /// </summary> + /// <param name="identifier">Either a URI or XRI identifier.</param> + /// <returns>An <see cref="Identifier"/> instance for the given value.</returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Some of these identifiers are not properly formatted to be Uris at this stage.")] + public static Identifier Parse(string identifier) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(identifier)); + Contract.Ensures(Contract.Result<Identifier>() != null); + + return Parse(identifier, false); + } + + /// <summary> + /// Parses an identifier string and automatically determines + /// whether it is an XRI or URI. + /// </summary> + /// <param name="identifier">Either a URI or XRI identifier.</param> + /// <param name="serializeExactValue">if set to <c>true</c> this Identifier will serialize exactly as given rather than in its normalized form.</param> + /// <returns> + /// An <see cref="Identifier"/> instance for the given value. + /// </returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Some of these identifiers are not properly formatted to be Uris at this stage.")] + public static Identifier Parse(string identifier, bool serializeExactValue) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(identifier)); + Contract.Ensures(Contract.Result<Identifier>() != null); + + Identifier id; + if (XriIdentifier.IsValidXri(identifier)) { + id = new XriIdentifier(identifier); + } else { + id = new UriIdentifier(identifier); + } + + id.IsDeserializedInstance = serializeExactValue; + return id; + } + + /// <summary> + /// Attempts to parse a string for an OpenId Identifier. + /// </summary> + /// <param name="value">The string to be parsed.</param> + /// <param name="result">The parsed Identifier form.</param> + /// <returns> + /// True if the operation was successful. False if the string was not a valid OpenId Identifier. + /// </returns> + public static bool TryParse(string value, out Identifier result) { + if (string.IsNullOrEmpty(value)) { + result = null; + return false; + } + + if (IsValid(value)) { + result = Parse(value); + return true; + } else { + result = null; + return false; + } + } + + /// <summary> + /// Checks the validity of a given string representation of some Identifier. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <returns> + /// <c>true</c> if the specified identifier is valid; otherwise, <c>false</c>. + /// </returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Some of these identifiers are not properly formatted to be Uris at this stage.")] + public static bool IsValid(string identifier) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(identifier)); + return XriIdentifier.IsValidXri(identifier) || UriIdentifier.IsValidUri(identifier); + } + + /// <summary> + /// Tests equality between two <see cref="Identifier"/>s. + /// </summary> + /// <param name="id1">The first Identifier.</param> + /// <param name="id2">The second Identifier.</param> + /// <returns> + /// <c>true</c> if the two instances should be considered equal; <c>false</c> otherwise. + /// </returns> + public static bool operator ==(Identifier id1, Identifier id2) { + return id1.EqualsNullSafe(id2); + } + + /// <summary> + /// Tests inequality between two <see cref="Identifier"/>s. + /// </summary> + /// <param name="id1">The first Identifier.</param> + /// <param name="id2">The second Identifier.</param> + /// <returns> + /// <c>true</c> if the two instances should be considered unequal; <c>false</c> if they are equal. + /// </returns> + public static bool operator !=(Identifier id1, Identifier id2) { + return !id1.EqualsNullSafe(id2); + } + + /// <summary> + /// Tests equality between two <see cref="Identifier"/>s. + /// </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) { + Debug.Fail("This should be overridden in every derived class."); + return base.Equals(obj); + } + + /// <summary> + /// Gets the hash code for an <see cref="Identifier"/> for storage in a hashtable. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + Debug.Fail("This should be overridden in every derived class."); + return base.GetHashCode(); + } + + /// <summary> + /// Reparses the specified identifier in order to be assured that the concrete type that + /// implements the identifier is one of the well-known ones. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <returns>Either <see cref="XriIdentifier"/> or <see cref="UriIdentifier"/>.</returns> + internal static Identifier Reparse(Identifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Ensures(Contract.Result<Identifier>() != null); + + return Parse(identifier, identifier.IsDeserializedInstance); + } + + /// <summary> + /// Returns an <see cref="Identifier"/> that has no URI fragment. + /// Quietly returns the original <see cref="Identifier"/> if it is not + /// a <see cref="UriIdentifier"/> or no fragment exists. + /// </summary> + /// <returns>A new <see cref="Identifier"/> instance if there was a + /// fragment to remove, otherwise this same instance..</returns> + [Pure] + internal abstract Identifier TrimFragment(); + + /// <summary> + /// Converts a given identifier to its secure equivalent. + /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS. + /// Discovery is made to require SSL for the entire resolution process. + /// </summary> + /// <param name="secureIdentifier"> + /// The newly created secure identifier. + /// If the conversion fails, <paramref name="secureIdentifier"/> retains + /// <i>this</i> identifiers identity, but will never discover any endpoints. + /// </param> + /// <returns> + /// True if the secure conversion was successful. + /// False if the Identifier was originally created with an explicit HTTP scheme. + /// </returns> + internal abstract bool TryRequireSsl(out Identifier secureIdentifier); + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/IdentifierContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/IdentifierContract.cs new file mode 100644 index 0000000..4af18e1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/IdentifierContract.cs @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------- +// <copyright file="IdentifierContract.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 DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Code Contract for the <see cref="Identifier"/> class. + /// </summary> + [ContractClassFor(typeof(Identifier))] + internal abstract class IdentifierContract : Identifier { + /// <summary> + /// Prevents a default instance of the IdentifierContract class from being created. + /// </summary> + private IdentifierContract() + : base(null, false) { + } + + /// <summary> + /// Returns an <see cref="Identifier"/> that has no URI fragment. + /// Quietly returns the original <see cref="Identifier"/> if it is not + /// a <see cref="UriIdentifier"/> or no fragment exists. + /// </summary> + /// <returns> + /// A new <see cref="Identifier"/> instance if there was a + /// fragment to remove, otherwise this same instance.. + /// </returns> + internal override Identifier TrimFragment() { + Contract.Ensures(Contract.Result<Identifier>() != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Converts a given identifier to its secure equivalent. + /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS. + /// Discovery is made to require SSL for the entire resolution process. + /// </summary> + /// <param name="secureIdentifier">The newly created secure identifier. + /// If the conversion fails, <paramref name="secureIdentifier"/> retains + /// <i>this</i> identifiers identity, but will never discover any endpoints.</param> + /// <returns> + /// True if the secure conversion was successful. + /// False if the Identifier was originally created with an explicit HTTP scheme. + /// </returns> + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + Contract.Ensures(Contract.ValueAtReturn(out secureIdentifier) != null); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/IdentifierDiscoveryResult.cs b/src/DotNetOpenAuth.OpenId/OpenId/IdentifierDiscoveryResult.cs new file mode 100644 index 0000000..c851f24 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/Interop/AuthenticationResponseShim.cs b/src/DotNetOpenAuth.OpenId/OpenId/Interop/AuthenticationResponseShim.cs new file mode 100644 index 0000000..c0354ac --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/Interop/ClaimsResponseShim.cs b/src/DotNetOpenAuth.OpenId/OpenId/Interop/ClaimsResponseShim.cs new file mode 100644 index 0000000..689777b --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/Interop/OpenIdRelyingPartyShim.cs b/src/DotNetOpenAuth.OpenId/OpenId/Interop/OpenIdRelyingPartyShim.cs new file mode 100644 index 0000000..fc0f32e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/Messages/AssociateDiffieHellmanRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateDiffieHellmanRequest.cs new file mode 100644 index 0000000..01e23e4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateDiffieHellmanRequest.cs @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateDiffieHellmanRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using Org.Mentalis.Security.Cryptography; + + /// <summary> + /// An OpenID direct request from Relying Party to Provider to initiate an association that uses Diffie-Hellman encryption. + /// </summary> + internal class AssociateDiffieHellmanRequest : AssociateRequest { + /// <summary> + /// The (only) value we use for the X variable in the Diffie-Hellman algorithm. + /// </summary> + internal static readonly int DefaultX = 1024; + + /// <summary> + /// The default gen value for the Diffie-Hellman algorithm. + /// </summary> + internal static readonly byte[] DefaultGen = { 2 }; + + /// <summary> + /// The default modulus value for the Diffie-Hellman algorithm. + /// </summary> + internal static readonly byte[] DefaultMod = { + 0, 220, 249, 58, 11, 136, 57, 114, 236, 14, 25, 152, 154, 197, 162, + 206, 49, 14, 29, 55, 113, 126, 141, 149, 113, 187, 118, 35, 115, 24, + 102, 230, 30, 247, 90, 46, 39, 137, 139, 5, 127, 152, 145, 194, 226, + 122, 99, 156, 63, 41, 182, 8, 20, 88, 28, 211, 178, 202, 57, 134, 210, + 104, 55, 5, 87, 125, 69, 194, 231, 229, 45, 200, 28, 122, 23, 24, 118, + 229, 206, 167, 75, 20, 72, 191, 223, 175, 24, 130, 142, 253, 37, 25, + 241, 78, 69, 227, 130, 102, 52, 175, 25, 73, 229, 181, 53, 204, 130, + 154, 72, 59, 138, 118, 34, 62, 93, 73, 10, 37, 127, 5, 189, 255, 22, + 242, 251, 34, 197, 131, 171 }; + + /// <summary> + /// Initializes a new instance of the <see cref="AssociateDiffieHellmanRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version this message must comply with.</param> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + internal AssociateDiffieHellmanRequest(Version version, Uri providerEndpoint) + : base(version, providerEndpoint) { + this.DiffieHellmanModulus = DefaultMod; + this.DiffieHellmanGen = DefaultGen; + } + + /// <summary> + /// Gets or sets the openid.dh_modulus value. + /// </summary> + /// <value>May be null if the default value given in the OpenID spec is to be used.</value> + [MessagePart("openid.dh_modulus", IsRequired = false, AllowEmpty = false)] + internal byte[] DiffieHellmanModulus { get; set; } + + /// <summary> + /// Gets or sets the openid.dh_gen value. + /// </summary> + /// <value>May be null if the default value given in the OpenID spec is to be used.</value> + [MessagePart("openid.dh_gen", IsRequired = false, AllowEmpty = false)] + internal byte[] DiffieHellmanGen { get; set; } + + /// <summary> + /// Gets or sets the openid.dh_consumer_public value. + /// </summary> + /// <remarks> + /// This property is initialized with a call to <see cref="InitializeRequest"/>. + /// </remarks> + [MessagePart("openid.dh_consumer_public", IsRequired = true, AllowEmpty = false)] + internal byte[] DiffieHellmanConsumerPublic { get; set; } + + /// <summary> + /// Gets the Diffie-Hellman algorithm. + /// </summary> + /// <remarks> + /// This property is initialized with a call to <see cref="InitializeRequest"/>. + /// </remarks> + internal DiffieHellman Algorithm { get; private set; } + + /// <summary> + /// Called by the Relying Party to initialize the Diffie-Hellman algorithm and consumer public key properties. + /// </summary> + internal void InitializeRequest() { + if (this.DiffieHellmanModulus == null || this.DiffieHellmanGen == null) { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, OpenIdStrings.DiffieHellmanRequiredPropertiesNotSet, string.Join(", ", new string[] { "DiffieHellmanModulus", "DiffieHellmanGen" }))); + } + + this.Algorithm = new DiffieHellmanManaged(this.DiffieHellmanModulus ?? DefaultMod, this.DiffieHellmanGen ?? DefaultGen, DefaultX); + byte[] consumerPublicKeyExchange = this.Algorithm.CreateKeyExchange(); + this.DiffieHellmanConsumerPublic = DiffieHellmanUtilities.EnsurePositive(consumerPublicKeyExchange); + } + + /// <summary> + /// Creates a Provider's response to an incoming association request. + /// </summary> + /// <returns> + /// The appropriate association response message. + /// </returns> + /// <remarks> + /// <para>If an association can be successfully created, the + /// <see cref="AssociateSuccessfulResponse.CreateAssociation"/> method must not be + /// called by this method.</para> + /// <para>Successful association response messages will derive from <see cref="AssociateSuccessfulResponse"/>. + /// Failed association response messages will derive from <see cref="AssociateUnsuccessfulResponse"/>.</para> + /// </remarks> + protected override IProtocolMessage CreateResponseCore() { + var response = new AssociateDiffieHellmanResponse(this.Version, this); + response.AssociationType = this.AssociationType; + return response; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateDiffieHellmanResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateDiffieHellmanResponse.cs new file mode 100644 index 0000000..5237826 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateDiffieHellmanResponse.cs @@ -0,0 +1,103 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateDiffieHellmanResponse.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 AssociateDiffieHellmanResponse : AssociateSuccessfulResponse { + /// <summary> + /// Initializes a new instance of the <see cref="AssociateDiffieHellmanResponse"/> class. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="originatingRequest">The originating request.</param> + internal AssociateDiffieHellmanResponse(Version responseVersion, AssociateDiffieHellmanRequest originatingRequest) + : base(responseVersion, originatingRequest) { + } + + /// <summary> + /// Gets or sets the Provider's Diffie-Hellman public key. + /// </summary> + /// <value>btwoc(g ^ xb mod p)</value> + [MessagePart("dh_server_public", IsRequired = true, AllowEmpty = false)] + internal byte[] DiffieHellmanServerPublic { get; set; } + + /// <summary> + /// Gets or sets the MAC key (shared secret), encrypted with the secret Diffie-Hellman value. + /// </summary> + /// <value>H(btwoc(g ^ (xa * xb) mod p)) XOR MAC key. H is either "SHA1" or "SHA256" depending on the session type. </value> + [MessagePart("enc_mac_key", IsRequired = true, AllowEmpty = false)] + internal byte[] EncodedMacKey { get; set; } + + /// <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; + } + + /// <summary> + /// Creates the association at the provider side after the association request has been received. + /// </summary> + /// <param name="request">The association request.</param> + /// <param name="associationStore">The OpenID Provider's association store or handle encoder.</param> + /// <param name="securitySettings">The security settings of the Provider.</param> + /// <returns> + /// The newly created association. + /// </returns> + /// <remarks> + /// The response message is updated to include the details of the created association by this method, + /// but the resulting association is <i>not</i> added to the association store and must be done by the caller. + /// </remarks> + protected override Association CreateAssociationAtProvider(AssociateRequest request, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + var diffieHellmanRequest = request as AssociateDiffieHellmanRequest; + ErrorUtilities.VerifyInternal(diffieHellmanRequest != null, "Expected a DH request type."); + + this.SessionType = this.SessionType ?? request.SessionType; + + // Go ahead and create the association first, complete with its secret that we're about to share. + Association association = HmacShaAssociation.Create(this.Protocol, this.AssociationType, AssociationRelyingPartyType.Smart, associationStore, securitySettings); + + // We now need to securely communicate the secret to the relying party using Diffie-Hellman. + // We do this by performing a DH algorithm on the secret and setting a couple of properties + // that will be transmitted to the Relying Party. The RP will perform an inverse operation + // using its part of a DH secret in order to decrypt the shared secret we just invented + // above when we created the association. + using (DiffieHellman dh = new DiffieHellmanManaged( + diffieHellmanRequest.DiffieHellmanModulus ?? AssociateDiffieHellmanRequest.DefaultMod, + diffieHellmanRequest.DiffieHellmanGen ?? AssociateDiffieHellmanRequest.DefaultGen, + AssociateDiffieHellmanRequest.DefaultX)) { + HashAlgorithm hasher = DiffieHellmanUtilities.Lookup(this.Protocol, this.SessionType); + this.DiffieHellmanServerPublic = DiffieHellmanUtilities.EnsurePositive(dh.CreateKeyExchange()); + this.EncodedMacKey = DiffieHellmanUtilities.SHAHashXorSecret(hasher, dh, diffieHellmanRequest.DiffieHellmanConsumerPublic, association.SecretKey); + } + return association; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateRequest.cs new file mode 100644 index 0000000..2a0dc7a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateRequest.cs @@ -0,0 +1,214 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An OpenID direct request from Relying Party to Provider to initiate an association. + /// </summary> + [DebuggerDisplay("OpenID {Version} {Mode} {AssociationType} {SessionType}")] + internal abstract class AssociateRequest : RequestBase { + /// <summary> + /// Initializes a new instance of the <see cref="AssociateRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version this message must comply with.</param> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + protected AssociateRequest(Version version, Uri providerEndpoint) + : base(version, providerEndpoint, GetProtocolConstant(version, p => p.Args.Mode.associate), MessageTransport.Direct) { + } + + /// <summary> + /// Gets or sets the preferred association type. The association type defines the algorithm to be used to sign subsequent messages. + /// </summary> + /// <value>Value: A valid association type from Section 8.3.</value> + [MessagePart("openid.assoc_type", IsRequired = true, AllowEmpty = false)] + internal string AssociationType { get; set; } + + /// <summary> + /// Gets or sets the preferred association session type. This defines the method used to encrypt the association's MAC key in transit. + /// </summary> + /// <value>Value: A valid association session type from Section 8.4 (Association Session Types). </value> + /// <remarks>Note: Unless using transport layer encryption, "no-encryption" MUST NOT be used. </remarks> + [MessagePart("openid.session_type", IsRequired = false, AllowEmpty = true)] + [MessagePart("openid.session_type", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] + internal string SessionType { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + ErrorUtilities.VerifyProtocol( + !string.Equals(this.SessionType, Protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal) || this.Recipient.IsTransportSecure(), + OpenIdStrings.NoEncryptionSessionRequiresHttps, + this); + } + + /// <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; + } + } + + /// <summary> + /// Creates a Provider's response to an incoming association request. + /// </summary> + /// <param name="associationStore">The association store.</param> + /// <param name="securitySettings">The security settings on the Provider.</param> + /// <returns> + /// The appropriate association response that is ready to be sent back to the Relying Party. + /// </returns> + /// <remarks> + /// <para>If an association is created, it will be automatically be added to the provided + /// association store.</para> + /// <para>Successful association response messages will derive from <see cref="AssociateSuccessfulResponse"/>. + /// Failed association response messages will derive from <see cref="AssociateUnsuccessfulResponse"/>.</para> + /// </remarks> + internal IProtocolMessage CreateResponse(IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(associationStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + + IProtocolMessage response; + if (securitySettings.IsAssociationInPermittedRange(Protocol, this.AssociationType) && + HmacShaAssociation.IsDHSessionCompatible(Protocol, this.AssociationType, this.SessionType)) { + response = this.CreateResponseCore(); + + // Create and store the association if this is a successful response. + var successResponse = response as AssociateSuccessfulResponse; + if (successResponse != null) { + successResponse.CreateAssociation(this, associationStore, securitySettings); + } + } else { + response = this.CreateUnsuccessfulResponse(securitySettings); + } + + return response; + } + + /// <summary> + /// Creates a Provider's response to an incoming association request. + /// </summary> + /// <returns> + /// The appropriate association response message. + /// </returns> + /// <remarks> + /// <para>If an association can be successfully created, the + /// <see cref="AssociateSuccessfulResponse.CreateAssociation"/> method must not be + /// called by this method.</para> + /// <para>Successful association response messages will derive from <see cref="AssociateSuccessfulResponse"/>. + /// Failed association response messages will derive from <see cref="AssociateUnsuccessfulResponse"/>.</para> + /// </remarks> + protected abstract IProtocolMessage CreateResponseCore(); + + /// <summary> + /// Creates a response that notifies the Relying Party that the requested + /// association type is not supported by this Provider, and offers + /// an alternative association type, if possible. + /// </summary> + /// <param name="securitySettings">The security settings that apply to this Provider.</param> + /// <returns>The response to send to the Relying Party.</returns> + private AssociateUnsuccessfulResponse CreateUnsuccessfulResponse(ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(securitySettings != null); + + var unsuccessfulResponse = new AssociateUnsuccessfulResponse(this.Version, this); + + // The strategy here is to suggest that the RP try again with the lowest + // permissible security settings, giving the RP the best chance of being + // able to match with a compatible request. + bool unencryptedAllowed = this.Recipient.IsTransportSecure(); + bool useDiffieHellman = !unencryptedAllowed; + string associationType, sessionType; + if (HmacShaAssociation.TryFindBestAssociation(Protocol, false, securitySettings, useDiffieHellman, out associationType, out sessionType)) { + ErrorUtilities.VerifyInternal(this.AssociationType != associationType, "The RP asked for an association that should have been allowed, but the OP is trying to suggest the same one as an alternative!"); + unsuccessfulResponse.AssociationType = associationType; + unsuccessfulResponse.SessionType = sessionType; + Logger.OpenId.InfoFormat( + "Association requested of type '{0}' and session '{1}', which the Provider does not support. Sending back suggested alternative of '{0}' with session '{1}'.", + this.AssociationType, + this.SessionType, + unsuccessfulResponse.AssociationType, + unsuccessfulResponse.SessionType); + } else { + Logger.OpenId.InfoFormat("Association requested of type '{0}' and session '{1}', which the Provider does not support. No alternative association type qualified for suggesting back to the Relying Party.", this.AssociationType, this.SessionType); + } + + return unsuccessfulResponse; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateSuccessfulResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateSuccessfulResponse.cs new file mode 100644 index 0000000..42d8816 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateSuccessfulResponse.cs @@ -0,0 +1,159 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateSuccessfulResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// The base class that all successful association response messages derive from. + /// </summary> + /// <remarks> + /// Association response messages are described in OpenID 2.0 section 8.2. This type covers section 8.2.1. + /// </remarks> + [DebuggerDisplay("OpenID {Version} associate response {AssociationHandle} {AssociationType} {SessionType}")] + [ContractClass(typeof(AssociateSuccessfulResponseContract))] + internal abstract class AssociateSuccessfulResponse : DirectResponseBase { + /// <summary> + /// A flag indicating whether an association has already been created. + /// </summary> + private bool associationCreated; + + /// <summary> + /// Initializes a new instance of the <see cref="AssociateSuccessfulResponse"/> class. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="originatingRequest">The originating request.</param> + internal AssociateSuccessfulResponse(Version responseVersion, AssociateRequest originatingRequest) + : base(responseVersion, originatingRequest) { + } + + /// <summary> + /// Gets or sets the association handle is used as a key to refer to this association in subsequent messages. + /// </summary> + /// <value>A string 255 characters or less in length. It MUST consist only of ASCII characters in the range 33-126 inclusive (printable non-whitespace characters). </value> + [MessagePart("assoc_handle", IsRequired = true, AllowEmpty = false)] + internal string AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the preferred association type. The association type defines the algorithm to be used to sign subsequent messages. + /// </summary> + /// <value>Value: A valid association type from Section 8.3.</value> + [MessagePart("assoc_type", IsRequired = true, AllowEmpty = false)] + internal string AssociationType { get; set; } + + /// <summary> + /// Gets or sets the value of the "openid.session_type" parameter from the request. + /// If the OP is unwilling or unable to support this association type, it MUST return an + /// unsuccessful response (Unsuccessful Response Parameters). + /// </summary> + /// <value>Value: A valid association session type from Section 8.4 (Association Session Types). </value> + /// <remarks>Note: Unless using transport layer encryption, "no-encryption" MUST NOT be used. </remarks> + [MessagePart("session_type", IsRequired = false, AllowEmpty = true)] + [MessagePart("session_type", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] + internal string SessionType { get; set; } + + /// <summary> + /// Gets or sets the lifetime, in seconds, of this association. The Relying Party MUST NOT use the association after this time has passed. + /// </summary> + /// <value>An integer, represented in base 10 ASCII. </value> + [MessagePart("expires_in", IsRequired = true)] + internal long ExpiresIn { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.Version.Major < 2) { + ErrorUtilities.VerifyProtocol( + string.IsNullOrEmpty(this.SessionType) || string.Equals(this.SessionType, this.Protocol.Args.SessionType.DH_SHA1, StringComparison.Ordinal), + MessagingStrings.UnexpectedMessagePartValueForConstant, + GetType().Name, + Protocol.openid.session_type, + this.Protocol.Args.SessionType.DH_SHA1, + this.SessionType); + } + } + + /// <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> + /// <param name="associationStore">The Provider's association store.</param> + /// <param name="securitySettings">The security settings for the Provider. Should be <c>null</c> for Relying Parties.</param> + /// <returns> + /// The created association. + /// </returns> + /// <remarks> + /// The response message is updated to include the details of the created association by this method. + /// This method is called by both the Provider and the Relying Party, but actually performs + /// quite different operations in either scenario. + /// </remarks> + internal Association CreateAssociation(AssociateRequest request, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(request != null); + ErrorUtilities.VerifyInternal(!this.associationCreated, "The association has already been created."); + + Association association; + + // If this message is outgoing, then we need to initialize some common + // properties based on the created association. + if (this.Incoming) { + association = this.CreateAssociationAtRelyingParty(request); + } else { + ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings"); + association = this.CreateAssociationAtProvider(request, associationStore, securitySettings); + this.ExpiresIn = association.SecondsTillExpiration; + this.AssociationHandle = association.Handle; + } + + this.associationCreated = true; + + return association; + } + + /// <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> + /// <param name="associationStore">The Provider's association store.</param> + /// <param name="securitySettings">The security settings of the Provider.</param> + /// <returns> + /// The created association. + /// </returns> + /// <remarks> + /// <para>The caller will update this message's <see cref="ExpiresIn"/> and <see cref="AssociationHandle"/> + /// properties based on the <see cref="Association"/> returned by this method, but any other + /// association type specific properties must be set by this method.</para> + /// <para>The response message is updated to include the details of the created association by this method, + /// but the resulting association is <i>not</i> added to the association store and must be done by the caller.</para> + /// </remarks> + protected abstract Association CreateAssociationAtProvider(AssociateRequest request, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings); + + /// <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/OpenId/Messages/AssociateSuccessfulResponseContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateSuccessfulResponseContract.cs new file mode 100644 index 0000000..d474608 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateSuccessfulResponseContract.cs @@ -0,0 +1,30 @@ +// <auto-generated /> + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Provider; + + [ContractClassFor(typeof(AssociateSuccessfulResponse))] + internal abstract class AssociateSuccessfulResponseContract : AssociateSuccessfulResponse { + protected AssociateSuccessfulResponseContract() : base(null, null) { + } + + protected override Association CreateAssociationAtProvider(AssociateRequest request, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentNullException>(associationStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + throw new NotImplementedException(); + } + + protected override Association CreateAssociationAtRelyingParty(AssociateRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnencryptedRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnencryptedRequest.cs new file mode 100644 index 0000000..ef302a5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnencryptedRequest.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateUnencryptedRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// Represents an association request that is sent using HTTPS and otherwise communicates the shared secret in plain text. + /// </summary> + internal class AssociateUnencryptedRequest : AssociateRequest { + /// <summary> + /// Initializes a new instance of the <see cref="AssociateUnencryptedRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version this message must comply with.</param> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + internal AssociateUnencryptedRequest(Version version, Uri providerEndpoint) + : base(version, providerEndpoint) { + SessionType = Protocol.Args.SessionType.NoEncryption; + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + ErrorUtilities.VerifyProtocol( + string.Equals(this.SessionType, Protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal), + MessagingStrings.UnexpectedMessagePartValueForConstant, + GetType().Name, + Protocol.openid.session_type, + Protocol.Args.SessionType.NoEncryption, + SessionType); + } + + /// <summary> + /// Creates a Provider's response to an incoming association request. + /// </summary> + /// <returns> + /// The appropriate association response message. + /// </returns> + /// <remarks> + /// <para>If an association can be successfully created, the + /// <see cref="AssociateSuccessfulResponse.CreateAssociation"/> method must not be + /// called by this method.</para> + /// <para>Successful association response messages will derive from <see cref="AssociateSuccessfulResponse"/>. + /// Failed association response messages will derive from <see cref="AssociateUnsuccessfulResponse"/>.</para> + /// </remarks> + protected override IProtocolMessage CreateResponseCore() { + var response = new AssociateUnencryptedResponse(this.Version, this); + response.AssociationType = this.AssociationType; + return response; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnencryptedResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnencryptedResponse.cs new file mode 100644 index 0000000..7e2194a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnencryptedResponse.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateUnencryptedResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// The successful unencrypted association response message. + /// </summary> + /// <remarks> + /// Association response messages are described in OpenID 2.0 section 8.2. This type covers section 8.2.2. + /// </remarks> + internal class AssociateUnencryptedResponse : AssociateSuccessfulResponse { + /// <summary> + /// Initializes a new instance of the <see cref="AssociateUnencryptedResponse"/> class. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="originatingRequest">The originating request.</param> + internal AssociateUnencryptedResponse(Version responseVersion, AssociateUnencryptedRequest originatingRequest) + : base(responseVersion, originatingRequest) { + SessionType = Protocol.Args.SessionType.NoEncryption; + } + + /// <summary> + /// Gets or sets the MAC key (shared secret) for this association, Base 64 (Josefsson, S., “The Base16, Base32, and Base64 Data Encodings,” .) [RFC3548] encoded. + /// </summary> + [MessagePart("mac_key", IsRequired = true, AllowEmpty = false)] + internal byte[] MacKey { get; set; } + + /// <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> + /// <param name="associationStore">The Provider's association store.</param> + /// <param name="securitySettings">The security settings of the Provider.</param> + /// <returns> + /// The created association. + /// </returns> + /// <remarks> + /// <para>The caller will update this message's + /// <see cref="AssociateSuccessfulResponse.ExpiresIn"/> and + /// <see cref="AssociateSuccessfulResponse.AssociationHandle"/> + /// properties based on the <see cref="Association"/> returned by this method, but any other + /// association type specific properties must be set by this method.</para> + /// <para>The response message is updated to include the details of the created association by this method, + /// but the resulting association is <i>not</i> added to the association store and must be done by the caller.</para> + /// </remarks> + protected override Association CreateAssociationAtProvider(AssociateRequest request, IProviderAssociationStore associationStore, ProviderSecuritySettings securitySettings) { + Association association = HmacShaAssociation.Create(Protocol, this.AssociationType, AssociationRelyingPartyType.Smart, associationStore, securitySettings); + this.MacKey = association.SecretKey; + return association; + } + + /// <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/OpenId/Messages/AssociateUnsuccessfulResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnsuccessfulResponse.cs new file mode 100644 index 0000000..71ecc05 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/AssociateUnsuccessfulResponse.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociateUnsuccessfulResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The Provider's response to a Relying Party that requested an association that the Provider does not support. + /// </summary> + /// <remarks> + /// This message type described in OpenID 2.0 section 8.2.4. + /// </remarks> + [DebuggerDisplay("OpenID {Version} associate (failed) response")] + internal class AssociateUnsuccessfulResponse : DirectErrorResponse { + /// <summary> + /// A hard-coded string indicating an error occurred. + /// </summary> + /// <value>"unsupported-type" </value> + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Read by reflection")] + [MessagePart("error_code", IsRequired = true, AllowEmpty = false)] +#pragma warning disable 0414 // read by reflection + private readonly string Error = "unsupported-type"; +#pragma warning restore 0414 + + /// <summary> + /// Initializes a new instance of the <see cref="AssociateUnsuccessfulResponse"/> class. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="originatingRequest">The originating request.</param> + internal AssociateUnsuccessfulResponse(Version responseVersion, AssociateRequest originatingRequest) + : base(responseVersion, originatingRequest) { + this.ErrorMessage = string.Format(CultureInfo.CurrentCulture, OpenIdStrings.AssociationOrSessionTypeUnrecognizedOrNotSupported, originatingRequest.AssociationType, originatingRequest.SessionType); + } + + /// <summary> + /// Gets or sets an association type supported by the OP from Section 8.3 (Association Types). + /// </summary> + [MessagePart("assoc_type", IsRequired = false, AllowEmpty = false)] + internal string AssociationType { get; set; } + + /// <summary> + /// Gets or sets a valid association session type from Section 8.4 (Association Session Types) that the OP supports. + /// </summary> + [MessagePart("session_type", IsRequired = false, AllowEmpty = false)] + internal string SessionType { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckAuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckAuthenticationRequest.cs new file mode 100644 index 0000000..db69d3d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckAuthenticationRequest.cs @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckAuthenticationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.ChannelElements; + + /// <summary> + /// A message a Relying Party sends to a Provider to confirm the validity + /// of a positive assertion that was signed by a Provider-only secret. + /// </summary> + /// <remarks> + /// The significant payload of this message depends entirely upon the + /// assertion message, and therefore is all in the + /// <see cref="DotNetOpenAuth.Messaging.IMessage.ExtraData"/> property bag. + /// </remarks> + internal class CheckAuthenticationRequest : RequestBase { + /// <summary> + /// Initializes a new instance of the <see cref="CheckAuthenticationRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version this message must comply with.</param> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + internal CheckAuthenticationRequest(Version version, Uri providerEndpoint) + : base(version, providerEndpoint, GetProtocolConstant(version, p => p.Args.Mode.check_authentication), MessageTransport.Direct) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CheckAuthenticationRequest"/> class + /// based on the contents of some signed message whose signature must be verified. + /// </summary> + /// <param name="message">The message whose signature should be verified.</param> + /// <param name="channel">The channel. This is used only within the constructor and is not stored in a field.</param> + internal CheckAuthenticationRequest(IndirectSignedResponse message, Channel channel) + : base(message.Version, message.ProviderEndpoint, GetProtocolConstant(message.Version, p => p.Args.Mode.check_authentication), MessageTransport.Direct) { + Contract.Requires<ArgumentNullException>(channel != null); + + // Copy all message parts from the id_res message into this one, + // except for the openid.mode parameter. + MessageDictionary checkPayload = channel.MessageDescriptions.GetAccessor(message, true); + MessageDictionary thisPayload = channel.MessageDescriptions.GetAccessor(this); + foreach (var pair in checkPayload) { + if (!string.Equals(pair.Key, this.Protocol.openid.mode)) { + thisPayload[pair.Key] = pair.Value; + } + } + } + + /// <summary> + /// Gets or sets a value indicating whether the signature being verified by this request + /// is in fact valid. + /// </summary> + /// <value><c>true</c> if the signature is valid; otherwise, <c>false</c>.</value> + /// <remarks> + /// This property is automatically set as the message is received by the channel's + /// signing binding element. + /// </remarks> + internal bool IsValid { get; set; } + + /// <summary> + /// Gets or sets the ReturnTo that existed in the original signed message. + /// </summary> + /// <remarks> + /// This exists strictly for convenience in recreating the <see cref="IndirectSignedResponse"/> + /// message. + /// </remarks> + [MessagePart("openid.return_to", IsRequired = true, AllowEmpty = false, Encoder = typeof(OriginalStringUriEncoder))] + [MessagePart("openid.return_to", IsRequired = false, AllowEmpty = false, MinVersion = "2.0", Encoder = typeof(OriginalStringUriEncoder))] + internal Uri ReturnTo { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckAuthenticationResponse.cs new file mode 100644 index 0000000..f4d5243 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckAuthenticationResponse.cs @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckAuthenticationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// The message sent from the Provider to the Relying Party to confirm/deny + /// the validity of an assertion that was signed by a private Provider secret. + /// </summary> + internal class CheckAuthenticationResponse : DirectResponseBase { + /// <summary> + /// Initializes a new instance of the <see cref="CheckAuthenticationResponse"/> class + /// for use by the Relying Party. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="request">The request that this message is responding to.</param> + internal CheckAuthenticationResponse(Version responseVersion, CheckAuthenticationRequest request) + : base(responseVersion, request) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="CheckAuthenticationResponse"/> class + /// for use by the Provider. + /// </summary> + /// <param name="request">The request that this message is responding to.</param> + /// <param name="provider">The OpenID Provider that is preparing to send this response.</param> + internal CheckAuthenticationResponse(CheckAuthenticationRequest request, OpenIdProvider provider) + : base(request.Version, request) { + Contract.Requires<ArgumentNullException>(provider != null); + + // The channel's binding elements have already set the request's IsValid property + // appropriately. We just copy it into the response message. + this.IsValid = request.IsValid; + + // Confirm the RP should invalidate the association handle only if the association + // is not valid (any longer). OpenID 2.0 section 11.4.2.2. + IndirectSignedResponse signedResponse = new IndirectSignedResponse(request, provider.Channel); + string invalidateHandle = ((ITamperResistantOpenIdMessage)signedResponse).InvalidateHandle; + if (!string.IsNullOrEmpty(invalidateHandle) && !provider.AssociationStore.IsValid(signedResponse, false, invalidateHandle)) { + this.InvalidateHandle = invalidateHandle; + } + } + + /// <summary> + /// Gets or sets a value indicating whether the signature of the verification request is valid. + /// </summary> + [MessagePart("is_valid", IsRequired = true)] + internal bool IsValid { get; set; } + + /// <summary> + /// Gets or sets the handle the relying party should invalidate if <see cref="IsValid"/> is true. + /// </summary> + /// <value>The "invalidate_handle" value sent in the verification request, if the OP confirms it is invalid.</value> + /// <remarks> + /// <para>If present in a verification response with "is_valid" set to "true", + /// the Relying Party SHOULD remove the corresponding association from + /// its store and SHOULD NOT send further authentication requests with + /// this handle.</para> + /// <para>This two-step process for invalidating associations is necessary + /// to prevent an attacker from invalidating an association at will by + /// adding "invalidate_handle" parameters to an authentication response.</para> + /// <para>For OpenID 1.1, we allow this to be present but empty to put up with poor implementations such as Blogger.</para> + /// </remarks> + [MessagePart("invalidate_handle", IsRequired = false, AllowEmpty = true, MaxVersion = "1.1")] + [MessagePart("invalidate_handle", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + internal string InvalidateHandle { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckIdRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckIdRequest.cs new file mode 100644 index 0000000..09c36a5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/CheckIdRequest.cs @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckIdRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An authentication request from a Relying Party to a Provider. + /// </summary> + /// <remarks> + /// This message type satisfies OpenID 2.0 section 9.1. + /// </remarks> + [DebuggerDisplay("OpenID {Version} {Mode} {ClaimedIdentifier}")] + [Serializable] + internal class CheckIdRequest : SignedResponseRequest { + /// <summary> + /// Initializes a new instance of the <see cref="CheckIdRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="providerEndpoint">The Provider endpoint that receives this message.</param> + /// <param name="mode"> + /// <see cref="AuthenticationRequestMode.Immediate"/> for asynchronous javascript clients; + /// <see cref="AuthenticationRequestMode.Setup"/> to allow the Provider to interact with the user in order to complete authentication. + /// </param> + internal CheckIdRequest(Version version, Uri providerEndpoint, AuthenticationRequestMode mode) : + base(version, providerEndpoint, mode) { + } + + /// <summary> + /// Gets or sets the Claimed Identifier. + /// </summary> + /// <remarks> + /// <para>"openid.claimed_id" and "openid.identity" SHALL be either both present or both absent. + /// If neither value is present, the assertion is not about an identifier, + /// and will contain other information in its payload, using extensions (Extensions). </para> + /// <para>It is RECOMMENDED that OPs accept XRI identifiers with or without the "xri://" prefix, as specified in the Normalization (Normalization) section. </para> + /// </remarks> + [MessagePart("openid.claimed_id", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] + internal Identifier ClaimedIdentifier { get; set; } + + /// <summary> + /// Gets or sets the OP Local Identifier. + /// </summary> + /// <value>The OP-Local Identifier. </value> + /// <remarks> + /// <para>If a different OP-Local Identifier is not specified, the claimed + /// identifier MUST be used as the value for openid.identity.</para> + /// <para>Note: If this is set to the special value + /// "http://specs.openid.net/auth/2.0/identifier_select" then the OP SHOULD + /// choose an Identifier that belongs to the end user. This parameter MAY + /// be omitted if the request is not about an identifier (for instance if + /// an extension is in use that makes the request meaningful without it; + /// see openid.claimed_id above). </para> + /// </remarks> + [MessagePart("openid.identity", IsRequired = true, AllowEmpty = false)] + internal Identifier LocalIdentifier { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.Protocol.ClaimedIdentifierForOPIdentifier != null) { + // Ensure that the claimed_id and identity parameters are either both the + // special identifier_select value or both NOT that value. + ErrorUtilities.VerifyProtocol( + (this.LocalIdentifier == this.Protocol.ClaimedIdentifierForOPIdentifier) == (this.ClaimedIdentifier == this.Protocol.ClaimedIdentifierForOPIdentifier), + OpenIdStrings.MatchingArgumentsExpected, + Protocol.openid.claimed_id, + Protocol.openid.identity, + Protocol.ClaimedIdentifierForOPIdentifier); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/DirectErrorResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/DirectErrorResponse.cs new file mode 100644 index 0000000..607a139 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/DirectErrorResponse.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// <copyright file="DirectErrorResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Net; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message sent from a Provider to a Relying Party in response to a direct message request that resulted in an error. + /// </summary> + /// <remarks> + /// This message must be sent with an HTTP status code of 400. + /// This class satisfies OpenID 2.0 section 5.1.2.2. + /// </remarks> + internal class DirectErrorResponse : DirectResponseBase, IErrorMessage, IHttpDirectResponse { + /// <summary> + /// Initializes a new instance of the <see cref="DirectErrorResponse"/> class. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="originatingRequest">The originating request.</param> + internal DirectErrorResponse(Version responseVersion, IDirectedProtocolMessage originatingRequest) + : base(responseVersion, originatingRequest) { + } + + #region IHttpDirectResponse Members + + /// <summary> + /// Gets the HTTP status code that the direct respones should be sent with. + /// </summary> + /// <value><see cref="HttpStatusCode.BadRequest"/></value> + HttpStatusCode IHttpDirectResponse.HttpStatusCode { + get { return HttpStatusCode.BadRequest; } + } + + /// <summary> + /// Gets the HTTP headers to add to the response. + /// </summary> + /// <value>May be an empty collection, but must not be <c>null</c>.</value> + WebHeaderCollection IHttpDirectResponse.Headers { + get { return new WebHeaderCollection(); } + } + + #endregion + + /// <summary> + /// Gets or sets a human-readable message indicating why the request failed. + /// </summary> + [MessagePart("error", IsRequired = true, AllowEmpty = true)] + public string ErrorMessage { get; set; } + + /// <summary> + /// Gets or sets the contact address for the administrator of the server. + /// </summary> + /// <value>The contact address may take any form, as it is intended to be displayed to a person. </value> + [MessagePart("contact", IsRequired = false, AllowEmpty = true)] + public string Contact { get; set; } + + /// <summary> + /// Gets or sets a reference token, such as a support ticket number or a URL to a news blog, etc. + /// </summary> + [MessagePart("reference", IsRequired = false, AllowEmpty = true)] + public string Reference { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/DirectResponseBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/DirectResponseBase.cs new file mode 100644 index 0000000..e7619bc --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/DirectResponseBase.cs @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------- +// <copyright file="DirectResponseBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A common base class for OpenID direct message responses. + /// </summary> + [DebuggerDisplay("OpenID {Version} response")] + internal class DirectResponseBase : IDirectResponseProtocolMessage { + /// <summary> + /// The openid.ns parameter in the message. + /// </summary> + /// <value>"http://specs.openid.net/auth/2.0" </value> + /// <remarks> + /// OpenID 2.0 Section 5.1.2: + /// This particular value MUST be present for the response to be a valid OpenID 2.0 response. + /// Future versions of the specification may define different values in order to allow message + /// recipients to properly interpret the request. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Read by reflection.")] + [MessagePart("ns", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] +#pragma warning disable 0414 // read by reflection + private readonly string OpenIdNamespace = Protocol.OpenId2Namespace; +#pragma warning restore 0414 + + /// <summary> + /// Backing store for the <see cref="OriginatingRequest"/> properties. + /// </summary> + private IDirectedProtocolMessage originatingRequest; + + /// <summary> + /// Backing store for the <see cref="Incoming"/> properties. + /// </summary> + private bool incoming; + + /// <summary> + /// The dictionary of parameters that are not part of the OpenID specification. + /// </summary> + private Dictionary<string, string> extraData = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="DirectResponseBase"/> class. + /// </summary> + /// <param name="responseVersion">The OpenID version of the response message.</param> + /// <param name="originatingRequest">The originating request. May be null in case the request is unrecognizable and this is an error response.</param> + protected DirectResponseBase(Version responseVersion, IDirectedProtocolMessage originatingRequest) { + Contract.Requires<ArgumentNullException>(responseVersion != null); + + this.Version = responseVersion; + this.originatingRequest = originatingRequest; + } + + #region IProtocolMessage Properties + + /// <summary> + /// Gets the version of the protocol this message is prepared to implement. + /// </summary> + /// <value>Version 2.0</value> + public Version Version { get; private set; } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + public MessageProtections RequiredProtection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + /// <value><see cref="MessageTransport.Direct"/></value> + public MessageTransport Transport { + get { return MessageTransport.Direct; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters included in the message. + /// </summary> + public IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + #endregion + + #region IDirectResponseProtocolMessage Members + + /// <summary> + /// Gets the originating request message that caused this response to be formed. + /// </summary> + /// <remarks> + /// This property may be null if the request message was undecipherable. + /// </remarks> + IDirectedProtocolMessage IDirectResponseProtocolMessage.OriginatingRequest { + get { return this.originatingRequest; } + } + + #endregion + + /// <summary> + /// Gets a value indicating whether this message was deserialized as an incoming message. + /// </summary> + protected internal bool Incoming { + get { return this.incoming; } + } + + /// <summary> + /// Gets the protocol used by this message. + /// </summary> + protected Protocol Protocol { + get { return Protocol.Lookup(this.Version); } + } + + /// <summary> + /// Gets the originating request message that caused this response to be formed. + /// </summary> + protected IDirectedProtocolMessage OriginatingRequest { + get { return this.originatingRequest; } + } + + #region IProtocolMessage methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public virtual void EnsureValidMessage() { + } + + #endregion + + /// <summary> + /// Sets a flag indicating that this message is received (as opposed to sent). + /// </summary> + internal void SetAsIncoming() { + this.incoming = true; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/IErrorMessage.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IErrorMessage.cs new file mode 100644 index 0000000..549b327 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IErrorMessage.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// <copyright file="IErrorMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Members found on error response messages sent from a Provider + /// to a Relying Party in response to direct and indirect message + /// requests that result in an error. + /// </summary> + internal interface IErrorMessage : IProtocolMessage { + /// <summary> + /// Gets or sets a human-readable message indicating why the request failed. + /// </summary> + string ErrorMessage { get; set; } + + /// <summary> + /// Gets or sets the contact address for the administrator of the server. + /// </summary> + /// <value>The contact address may take any form, as it is intended to be displayed to a person. </value> + string Contact { get; set; } + + /// <summary> + /// Gets or sets a reference token, such as a support ticket number or a URL to a news blog, etc. + /// </summary> + string Reference { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/IOpenIdMessageExtension.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IOpenIdMessageExtension.cs new file mode 100644 index 0000000..08e02ba --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IOpenIdMessageExtension.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdMessageExtension.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The contract any OpenID extension for DotNetOpenAuth must implement. + /// </summary> + /// <remarks> + /// Classes that implement this interface should be marked as + /// [<see cref="SerializableAttribute"/>] to allow serializing state servers + /// to cache messages, particularly responses. + /// </remarks> + [ContractClass(typeof(IOpenIdMessageExtensionContract))] + public interface IOpenIdMessageExtension : IExtensionMessage { + /// <summary> + /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements. + /// </summary> + string TypeUri { get; } + + /// <summary> + /// Gets the additional TypeURIs that are supported by this extension, in preferred order. + /// May be empty if none other than <see cref="TypeUri"/> is supported, but + /// should not be null. + /// </summary> + /// <remarks> + /// Useful for reading in messages with an older version of an extension. + /// The value in the <see cref="TypeUri"/> property is always checked before + /// trying this list. + /// If you do support multiple versions of an extension using this method, + /// consider adding a CreateResponse method to your request extension class + /// so that the response can have the context it needs to remain compatible + /// given the version of the extension in the request message. + /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example. + /// </remarks> + IEnumerable<string> AdditionalSupportedTypeUris { get; } + + /// <summary> + /// Gets or sets a value indicating whether this extension was + /// signed by the sender. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the sender; otherwise, <c>false</c>. + /// </value> + bool IsSignedByRemoteParty { get; set; } + } + + /// <summary> + /// Code contract class for the IOpenIdMessageExtension interface. + /// </summary> + [ContractClassFor(typeof(IOpenIdMessageExtension))] + internal abstract class IOpenIdMessageExtensionContract : IOpenIdMessageExtension { + /// <summary> + /// Prevents a default instance of the <see cref="IOpenIdMessageExtensionContract"/> class from being created. + /// </summary> + private IOpenIdMessageExtensionContract() { + } + + #region IOpenIdMessageExtension Members + + /// <summary> + /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements. + /// </summary> + string IOpenIdMessageExtension.TypeUri { + get { + Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>())); + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets the additional TypeURIs that are supported by this extension, in preferred order. + /// May be empty if none other than <see cref="IOpenIdMessageExtension.TypeUri"/> is supported, but + /// should not be null. + /// </summary> + /// <remarks> + /// Useful for reading in messages with an older version of an extension. + /// The value in the <see cref="IOpenIdMessageExtension.TypeUri"/> property is always checked before + /// trying this list. + /// If you do support multiple versions of an extension using this method, + /// consider adding a CreateResponse method to your request extension class + /// so that the response can have the context it needs to remain compatible + /// given the version of the extension in the request message. + /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example. + /// </remarks> + IEnumerable<string> IOpenIdMessageExtension.AdditionalSupportedTypeUris { + get { + Contract.Ensures(Contract.Result<IEnumerable<string>>() != null); + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets or sets a value indicating whether this extension was + /// signed by the sender. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the sender; otherwise, <c>false</c>. + /// </value> + bool IOpenIdMessageExtension.IsSignedByRemoteParty { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + #endregion + + #region IMessage Members + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + Version IMessage.Version { + get { + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + IDictionary<string, string> IMessage.ExtraData { + get { + throw new NotImplementedException(); + } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + void IMessage.EnsureValidMessage() { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectErrorResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectErrorResponse.cs new file mode 100644 index 0000000..eb006db --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectErrorResponse.cs @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------- +// <copyright file="IndirectErrorResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message sent from a Provider to a Relying Party in response to an indirect message request that resulted in an error. + /// </summary> + /// <remarks> + /// This class satisfies OpenID 2.0 section 5.2.3. + /// </remarks> + internal class IndirectErrorResponse : IndirectResponseBase, IErrorMessage { + /// <summary> + /// Initializes a new instance of the <see cref="IndirectErrorResponse"/> class. + /// </summary> + /// <param name="request">The request that resulted in this error on the Provider.</param> + internal IndirectErrorResponse(SignedResponseRequest request) + : base(request, Protocol.Lookup(GetVersion(request)).openidnp.error) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectErrorResponse"/> class. + /// </summary> + /// <param name="version">The OpenID version this message should comply with.</param> + /// <param name="recipient">The recipient of this message.</param> + internal IndirectErrorResponse(Version version, Uri recipient) + : base(version, recipient, Protocol.Lookup(version).openidnp.error) { + } + + /// <summary> + /// Gets or sets a human-readable message indicating why the request failed. + /// </summary> + [MessagePart("openid.error", IsRequired = true, AllowEmpty = true)] + public string ErrorMessage { get; set; } + + /// <summary> + /// Gets or sets the contact address for the administrator of the server. + /// </summary> + /// <value>The contact address may take any form, as it is intended to be displayed to a person. </value> + [MessagePart("openid.contact", IsRequired = false, AllowEmpty = true)] + public string Contact { get; set; } + + /// <summary> + /// Gets or sets a reference token, such as a support ticket number or a URL to a news blog, etc. + /// </summary> + [MessagePart("openid.reference", IsRequired = false, AllowEmpty = true)] + public string Reference { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectResponseBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectResponseBase.cs new file mode 100644 index 0000000..fce6028 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectResponseBase.cs @@ -0,0 +1,113 @@ +//----------------------------------------------------------------------- +// <copyright file="IndirectResponseBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A common base class from which indirect response messages should derive. + /// </summary> + [Serializable] + internal class IndirectResponseBase : RequestBase, IProtocolMessageWithExtensions { + /// <summary> + /// Backing store for the <see cref="Extensions"/> property. + /// </summary> + private IList<IExtensionMessage> extensions = new List<IExtensionMessage>(); + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectResponseBase"/> class. + /// </summary> + /// <param name="request">The request that caused this response message to be constructed.</param> + /// <param name="mode">The value of the openid.mode parameter.</param> + protected IndirectResponseBase(SignedResponseRequest request, string mode) + : base(GetVersion(request), GetReturnTo(request), mode, MessageTransport.Indirect) { + Contract.Requires<ArgumentNullException>(request != null); + + this.OriginatingRequest = request; + } + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectResponseBase"/> class + /// for unsolicited assertion scenarios. + /// </summary> + /// <param name="version">The OpenID version supported at the Relying Party.</param> + /// <param name="relyingPartyReturnTo"> + /// The URI at which the Relying Party receives OpenID indirect messages. + /// </param> + /// <param name="mode">The value to use for the openid.mode parameter.</param> + protected IndirectResponseBase(Version version, Uri relyingPartyReturnTo, string mode) + : base(version, relyingPartyReturnTo, mode, MessageTransport.Indirect) { + } + + #region IProtocolMessageWithExtensions Members + + /// <summary> + /// Gets the list of extensions that are included with this message. + /// </summary> + /// <value></value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + public IList<IExtensionMessage> Extensions { + get { return this.extensions; } + } + + #endregion + + /// <summary> + /// Gets the signed extensions on this message. + /// </summary> + internal IEnumerable<IOpenIdMessageExtension> SignedExtensions { + get { return this.extensions.OfType<IOpenIdMessageExtension>().Where(ext => ext.IsSignedByRemoteParty); } + } + + /// <summary> + /// Gets the unsigned extensions on this message. + /// </summary> + internal IEnumerable<IOpenIdMessageExtension> UnsignedExtensions { + get { return this.extensions.OfType<IOpenIdMessageExtension>().Where(ext => !ext.IsSignedByRemoteParty); } + } + + /// <summary> + /// Gets the originating request message, if applicable. + /// </summary> + protected SignedResponseRequest OriginatingRequest { get; private set; } + + /// <summary> + /// Gets the <see cref="IMessage.Version"/> property of a message. + /// </summary> + /// <param name="message">The message to fetch the protocol version from.</param> + /// <returns>The value of the <see cref="IMessage.Version"/> property.</returns> + /// <remarks> + /// This method can be used by a constructor to throw an <see cref="ArgumentNullException"/> + /// instead of a <see cref="NullReferenceException"/>. + /// </remarks> + internal static Version GetVersion(IProtocolMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + return message.Version; + } + + /// <summary> + /// Gets the <see cref="SignedResponseRequest.ReturnTo"/> property of a message. + /// </summary> + /// <param name="message">The message to fetch the ReturnTo from.</param> + /// <returns>The value of the <see cref="SignedResponseRequest.ReturnTo"/> property.</returns> + /// <remarks> + /// This method can be used by a constructor to throw an <see cref="ArgumentNullException"/> + /// instead of a <see cref="NullReferenceException"/>. + /// </remarks> + private static Uri GetReturnTo(SignedResponseRequest message) { + Contract.Requires<ArgumentNullException>(message != null); + ErrorUtilities.VerifyProtocol(message.ReturnTo != null, OpenIdStrings.ReturnToRequiredForResponse); + return message.ReturnTo; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectSignedResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectSignedResponse.cs new file mode 100644 index 0000000..baeae16 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/IndirectSignedResponse.cs @@ -0,0 +1,409 @@ +//----------------------------------------------------------------------- +// <copyright file="IndirectSignedResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Net.Security; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OpenId.ChannelElements; + + /// <summary> + /// An indirect message from a Provider to a Relying Party where at least part of the + /// payload is signed so the Relying Party can verify it has not been tampered with. + /// </summary> + [DebuggerDisplay("OpenID {Version} {Mode} (no id assertion)")] + [Serializable] + internal class IndirectSignedResponse : IndirectResponseBase, ITamperResistantOpenIdMessage { + /// <summary> + /// The allowed date/time formats for the response_nonce parameter. + /// </summary> + /// <remarks> + /// This array of formats is not yet a complete list. + /// </remarks> + private static readonly string[] PermissibleDateTimeFormats = { "yyyy-MM-ddTHH:mm:ssZ" }; + + /// <summary> + /// Backing field for the <see cref="IExpiringProtocolMessage.UtcCreationDate"/> property. + /// </summary> + /// <remarks> + /// The field initializer being DateTime.UtcNow allows for OpenID 1.x messages + /// to pass through the StandardExpirationBindingElement. + /// </remarks> + private DateTime creationDateUtc = DateTime.UtcNow; + + /// <summary> + /// Backing store for the <see cref="ReturnToParameters"/> property. + /// </summary> + private IDictionary<string, string> returnToParameters; + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectSignedResponse"/> class. + /// </summary> + /// <param name="request"> + /// The authentication request that caused this assertion to be generated. + /// </param> + internal IndirectSignedResponse(SignedResponseRequest request) + : base(request, Protocol.Lookup(GetVersion(request)).Args.Mode.id_res) { + Contract.Requires<ArgumentNullException>(request != null); + + this.ReturnTo = request.ReturnTo; + this.ProviderEndpoint = request.Recipient.StripQueryArgumentsWithPrefix(Protocol.openid.Prefix); + ((ITamperResistantOpenIdMessage)this).AssociationHandle = request.AssociationHandle; + } + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectSignedResponse"/> class + /// in order to perform signature verification at the Provider. + /// </summary> + /// <param name="previouslySignedMessage">The previously signed message.</param> + /// <param name="channel">The channel. This is used only within the constructor and is not stored in a field.</param> + internal IndirectSignedResponse(CheckAuthenticationRequest previouslySignedMessage, Channel channel) + : base(GetVersion(previouslySignedMessage), previouslySignedMessage.ReturnTo, Protocol.Lookup(GetVersion(previouslySignedMessage)).Args.Mode.id_res) { + Contract.Requires<ArgumentNullException>(channel != null); + + // Copy all message parts from the check_authentication message into this one, + // except for the openid.mode parameter. + MessageDictionary checkPayload = channel.MessageDescriptions.GetAccessor(previouslySignedMessage); + MessageDictionary thisPayload = channel.MessageDescriptions.GetAccessor(this); + foreach (var pair in checkPayload) { + if (!string.Equals(pair.Key, this.Protocol.openid.mode)) { + thisPayload[pair.Key] = pair.Value; + } + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="IndirectSignedResponse"/> class + /// for unsolicited assertions. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="relyingPartyReturnTo">The return_to URL of the Relying Party. + /// This value will commonly be from <see cref="SignedResponseRequest.ReturnTo"/>, + /// but for unsolicited assertions may come from the Provider performing RP discovery + /// to find the appropriate return_to URL to use.</param> + internal IndirectSignedResponse(Version version, Uri relyingPartyReturnTo) + : base(version, relyingPartyReturnTo, Protocol.Lookup(version).Args.Mode.id_res) { + this.ReturnTo = relyingPartyReturnTo; + } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + /// <value> + /// <see cref="MessageProtections.All"/> for OpenID 2.0 messages. + /// <see cref="MessageProtections.TamperProtection"/> for OpenID 1.x messages. + /// </value> + /// <remarks> + /// Although the required protection is reduced for OpenID 1.x, + /// this library will provide Relying Party hosts with all protections + /// by adding its own specially-crafted nonce to the authentication request + /// messages except for stateless RPs in OpenID 1.x messages. + /// </remarks> + public override MessageProtections RequiredProtection { + // We actually manage to provide All protections regardless of OpenID version + // on both the Provider and Relying Party side, except for stateless RPs for OpenID 1.x. + get { return this.Version.Major < 2 ? MessageProtections.TamperProtection : MessageProtections.All; } + } + + /// <summary> + /// Gets or sets the message signature. + /// </summary> + /// <value>Base 64 encoded signature calculated as specified in Section 6 (Generating Signatures).</value> + [MessagePart("openid.sig", IsRequired = true, AllowEmpty = false)] + string ITamperResistantProtocolMessage.Signature { get; set; } + + /// <summary> + /// Gets or sets the signed parameter order. + /// </summary> + /// <value>Comma-separated list of signed fields.</value> + /// <example>"op_endpoint,identity,claimed_id,return_to,assoc_handle,response_nonce"</example> + /// <remarks> + /// This entry consists of the fields without the "openid." prefix that the signature covers. + /// This list MUST contain at least "op_endpoint", "return_to" "response_nonce" and "assoc_handle", + /// and if present in the response, "claimed_id" and "identity". + /// Additional keys MAY be signed as part of the message. See Generating Signatures. + /// </remarks> + [MessagePart("openid.signed", IsRequired = true, AllowEmpty = false)] + string ITamperResistantOpenIdMessage.SignedParameterOrder { get; set; } + + /// <summary> + /// Gets or sets the association handle used to sign the message. + /// </summary> + /// <value>The handle for the association that was used to sign this assertion. </value> + [MessagePart("openid.assoc_handle", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + [MessagePart("openid.assoc_handle", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.None, MaxVersion = "1.1")] + string ITamperResistantOpenIdMessage.AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the nonce that will protect the message from replay attacks. + /// </summary> + string IReplayProtectedProtocolMessage.Nonce { get; set; } + + /// <summary> + /// Gets the context within which the nonce must be unique. + /// </summary> + string IReplayProtectedProtocolMessage.NonceContext { + get { + if (this.ProviderEndpoint != null) { + return this.ProviderEndpoint.AbsoluteUri; + } else { + // This is the Provider, on an OpenID 1.x check_authentication message. + // We don't need any special nonce context because the Provider + // generated and consumed the nonce. + return string.Empty; + } + } + } + + /// <summary> + /// Gets or sets the UTC date/time the message was originally sent onto the network. + /// </summary> + /// <remarks> + /// The property setter should ensure a UTC date/time, + /// and throw an exception if this is not possible. + /// </remarks> + /// <exception cref="ArgumentException"> + /// Thrown when a DateTime that cannot be converted to UTC is set. + /// </exception> + DateTime IExpiringProtocolMessage.UtcCreationDate { + get { return this.creationDateUtc; } + set { this.creationDateUtc = value.ToUniversalTimeSafe(); } + } + + /// <summary> + /// Gets or sets the association handle that the Provider wants the Relying Party to not use any more. + /// </summary> + /// <value>If the Relying Party sent an invalid association handle with the request, it SHOULD be included here.</value> + /// <remarks> + /// For OpenID 1.1, we allow this to be present but empty to put up with poor implementations such as Blogger. + /// </remarks> + [MessagePart("openid.invalidate_handle", IsRequired = false, AllowEmpty = true, MaxVersion = "1.1")] + [MessagePart("openid.invalidate_handle", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + string ITamperResistantOpenIdMessage.InvalidateHandle { get; set; } + + /// <summary> + /// Gets or sets the Provider Endpoint URI. + /// </summary> + [MessagePart("openid.op_endpoint", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + internal Uri ProviderEndpoint { get; set; } + + /// <summary> + /// Gets or sets the return_to parameter as the relying party provided + /// it in <see cref="SignedResponseRequest.ReturnTo"/>. + /// </summary> + /// <value>Verbatim copy of the return_to URL parameter sent in the + /// request, before the Provider modified it. </value> + [MessagePart("openid.return_to", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, Encoder = typeof(OriginalStringUriEncoder))] + internal Uri ReturnTo { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the <see cref="ReturnTo"/> + /// URI's query string is unaltered between when the Relying Party + /// sent the original request and when the response was received. + /// </summary> + /// <remarks> + /// This property is not persisted in the transmitted message, and + /// has no effect on the Provider-side of the communication. + /// </remarks> + internal bool ReturnToParametersSignatureValidated { get; set; } + + /// <summary> + /// Gets or sets the nonce that will protect the message from replay attacks. + /// </summary> + /// <value> + /// <para>A string 255 characters or less in length, that MUST be unique to + /// this particular successful authentication response. The nonce MUST start + /// with the current time on the server, and MAY contain additional ASCII + /// characters in the range 33-126 inclusive (printable non-whitespace characters), + /// as necessary to make each response unique. The date and time MUST be + /// formatted as specified in section 5.6 of [RFC3339] + /// (Klyne, G. and C. Newman, “Date and Time on the Internet: Timestamps,” .), + /// with the following restrictions:</para> + /// <list type="bullet"> + /// <item>All times must be in the UTC timezone, indicated with a "Z".</item> + /// <item>No fractional seconds are allowed</item> + /// </list> + /// </value> + /// <example>2005-05-15T17:11:51ZUNIQUE</example> + internal string ResponseNonceTestHook { + get { return this.ResponseNonce; } + set { this.ResponseNonce = value; } + } + + /// <summary> + /// Gets or sets the nonce that will protect the message from replay attacks. + /// </summary> + /// <value> + /// <para>A string 255 characters or less in length, that MUST be unique to + /// this particular successful authentication response. The nonce MUST start + /// with the current time on the server, and MAY contain additional ASCII + /// characters in the range 33-126 inclusive (printable non-whitespace characters), + /// as necessary to make each response unique. The date and time MUST be + /// formatted as specified in section 5.6 of [RFC3339] + /// (Klyne, G. and C. Newman, “Date and Time on the Internet: Timestamps,” .), + /// with the following restrictions:</para> + /// <list type="bullet"> + /// <item>All times must be in the UTC timezone, indicated with a "Z".</item> + /// <item>No fractional seconds are allowed</item> + /// </list> + /// </value> + /// <example>2005-05-15T17:11:51ZUNIQUE</example> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by messaging framework via reflection.")] + [MessagePart("openid.response_nonce", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + [MessagePart("openid.response_nonce", IsRequired = false, AllowEmpty = false, RequiredProtection = ProtectionLevel.None, MaxVersion = "1.1")] + private string ResponseNonce { + get { + string uniqueFragment = ((IReplayProtectedProtocolMessage)this).Nonce; + return this.creationDateUtc.ToString(PermissibleDateTimeFormats[0], CultureInfo.InvariantCulture) + uniqueFragment; + } + + set { + if (value == null) { + ((IReplayProtectedProtocolMessage)this).Nonce = null; + } else { + int indexOfZ = value.IndexOf("Z", StringComparison.Ordinal); + ErrorUtilities.VerifyProtocol(indexOfZ >= 0, MessagingStrings.UnexpectedMessagePartValue, Protocol.openid.response_nonce, value); + this.creationDateUtc = DateTime.Parse(value.Substring(0, indexOfZ + 1), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); + ((IReplayProtectedProtocolMessage)this).Nonce = value.Substring(indexOfZ + 1); + } + } + } + + /// <summary> + /// Gets the querystring key=value pairs in the return_to URL. + /// </summary> + private IDictionary<string, string> ReturnToParameters { + get { + if (this.returnToParameters == null) { + this.returnToParameters = HttpUtility.ParseQueryString(this.ReturnTo.Query).ToDictionary(); + } + + return this.returnToParameters; + } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + this.VerifyReturnToMatchesRecipient(); + } + + /// <summary> + /// Gets the value of a named parameter in the return_to URL without signature protection. + /// </summary> + /// <param name="key">The full name of the parameter whose value is being sought.</param> + /// <returns>The value of the parameter if it is present and unaltered from when + /// the Relying Party signed it; <c>null</c> otherwise.</returns> + /// <remarks> + /// This method will always return null on the Provider-side, since Providers + /// cannot verify the private signature made by the relying party. + /// </remarks> + internal string GetReturnToArgument(string key) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(key)); + ErrorUtilities.VerifyInternal(this.ReturnTo != null, "ReturnTo was expected to be required but is null."); + + string value; + this.ReturnToParameters.TryGetValue(key, out value); + return value; + } + + /// <summary> + /// Gets the names of the callback parameters added to the original authentication request + /// without signature protection. + /// </summary> + /// <returns>A sequence of the callback parameter names.</returns> + internal IEnumerable<string> GetReturnToParameterNames() { + return this.ReturnToParameters.Keys; + } + + /// <summary> + /// Gets a dictionary of all the message part names and values + /// that are included in the message signature. + /// </summary> + /// <param name="channel">The channel.</param> + /// <returns> + /// A dictionary of the signed message parts. + /// </returns> + internal IDictionary<string, string> GetSignedMessageParts(Channel channel) { + Contract.Requires<ArgumentNullException>(channel != null); + + ITamperResistantOpenIdMessage signedSelf = this; + if (signedSelf.SignedParameterOrder == null) { + return EmptyDictionary<string, string>.Instance; + } + + MessageDictionary messageDictionary = channel.MessageDescriptions.GetAccessor(this); + string[] signedPartNamesWithoutPrefix = signedSelf.SignedParameterOrder.Split(','); + Dictionary<string, string> signedParts = new Dictionary<string, string>(signedPartNamesWithoutPrefix.Length); + + var signedPartNames = signedPartNamesWithoutPrefix.Select(part => Protocol.openid.Prefix + part); + foreach (string partName in signedPartNames) { + signedParts[partName] = messageDictionary[partName]; + } + + return signedParts; + } + + /// <summary> + /// Determines whether one querystring contains every key=value pair that + /// another querystring contains. + /// </summary> + /// <param name="superset">The querystring that should contain at least all the key=value pairs of the other.</param> + /// <param name="subset">The querystring containing the set of key=value pairs to test for in the other.</param> + /// <returns> + /// <c>true</c> if <paramref name="superset"/> contains all the query parameters that <paramref name="subset"/> does; <c>false</c> otherwise. + /// </returns> + private static bool IsQuerySubsetOf(string superset, string subset) { + NameValueCollection subsetArgs = HttpUtility.ParseQueryString(subset); + NameValueCollection supersetArgs = HttpUtility.ParseQueryString(superset); + return subsetArgs.Keys.Cast<string>().All(key => string.Equals(subsetArgs[key], supersetArgs[key], StringComparison.Ordinal)); + } + + /// <summary> + /// Verifies that the openid.return_to field matches the URL of the actual HTTP request. + /// </summary> + /// <remarks> + /// From OpenId Authentication 2.0 section 11.1: + /// To verify that the "openid.return_to" URL matches the URL that is processing this assertion: + /// * The URL scheme, authority, and path MUST be the same between the two URLs. + /// * Any query parameters that are present in the "openid.return_to" URL MUST + /// also be present with the same values in the URL of the HTTP request the RP received. + /// </remarks> + private void VerifyReturnToMatchesRecipient() { + ErrorUtilities.VerifyProtocol( + string.Equals(this.Recipient.Scheme, this.ReturnTo.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Recipient.Authority, this.ReturnTo.Authority, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Recipient.AbsolutePath, this.ReturnTo.AbsolutePath, StringComparison.Ordinal) && + IsQuerySubsetOf(this.Recipient.Query, this.ReturnTo.Query), + OpenIdStrings.ReturnToParamDoesNotMatchRequestUrl, + Protocol.openid.return_to, + this.ReturnTo, + this.Recipient); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/NegativeAssertionResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/NegativeAssertionResponse.cs new file mode 100644 index 0000000..52ff884 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/NegativeAssertionResponse.cs @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------- +// <copyright file="NegativeAssertionResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// The message OpenID Providers send back to Relying Parties to refuse + /// to assert the identity of a user. + /// </summary> + [Serializable] + internal class NegativeAssertionResponse : IndirectResponseBase { + /// <summary> + /// Initializes a new instance of the <see cref="NegativeAssertionResponse"/> class. + /// </summary> + /// <param name="request">The request that the relying party sent.</param> + internal NegativeAssertionResponse(CheckIdRequest request) + : this(request, null) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="NegativeAssertionResponse"/> class. + /// </summary> + /// <param name="request">The request that the relying party sent.</param> + /// <param name="channel">The channel to use to simulate construction of the user_setup_url, if applicable. May be null, but the user_setup_url will not be constructed.</param> + internal NegativeAssertionResponse(SignedResponseRequest request, Channel channel) + : base(request, GetMode(request)) { + // If appropriate, and when we're provided with a channel to do it, + // go ahead and construct the user_setup_url + if (this.Version.Major < 2 && request.Immediate && channel != null) { + // All requests are CheckIdRequests in OpenID 1.x, so this cast should be safe. + this.UserSetupUrl = ConstructUserSetupUrl((CheckIdRequest)request, channel); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="NegativeAssertionResponse"/> class. + /// </summary> + /// <param name="version">The version.</param> + /// <param name="relyingPartyReturnTo">The relying party return to.</param> + /// <param name="mode">The value of the openid.mode parameter.</param> + internal NegativeAssertionResponse(Version version, Uri relyingPartyReturnTo, string mode) + : base(version, relyingPartyReturnTo, mode) { + } + + /// <summary> + /// Gets or sets the URL the relying party can use to upgrade their authentication + /// request from an immediate to a setup message. + /// </summary> + /// <value>URL to redirect User-Agent to so the End User can do whatever's necessary to fulfill the assertion.</value> + /// <remarks> + /// This part is only included in OpenID 1.x responses. + /// </remarks> + [MessagePart("openid.user_setup_url", AllowEmpty = false, IsRequired = false, MaxVersion = "1.1")] + internal Uri UserSetupUrl { get; set; } + + /// <summary> + /// Gets a value indicating whether this <see cref="NegativeAssertionResponse"/> + /// is in response to an authentication request made in immediate mode. + /// </summary> + /// <value><c>true</c> if the request was in immediate mode; otherwise, <c>false</c>.</value> + internal bool Immediate { + get { + if (this.OriginatingRequest != null) { + return this.OriginatingRequest.Immediate; + } else { + if (String.Equals(this.Mode, Protocol.Args.Mode.setup_needed, StringComparison.Ordinal)) { + return true; + } else if (String.Equals(this.Mode, Protocol.Args.Mode.cancel, StringComparison.Ordinal)) { + return false; + } else { + throw ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessagePartValue, Protocol.openid.mode, this.Mode); + } + } + } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + // Since there are a couple of negative assertion modes, ensure that the mode given is one of the allowed ones. + ErrorUtilities.VerifyProtocol(String.Equals(this.Mode, Protocol.Args.Mode.setup_needed, StringComparison.Ordinal) || String.Equals(this.Mode, Protocol.Args.Mode.cancel, StringComparison.Ordinal), MessagingStrings.UnexpectedMessagePartValue, Protocol.openid.mode, this.Mode); + + if (this.Immediate && Protocol.Version.Major < 2) { + ErrorUtilities.VerifyProtocol(this.UserSetupUrl != null, OpenIdStrings.UserSetupUrlRequiredInImmediateNegativeResponse); + } + } + + /// <summary> + /// Constructs the value for the user_setup_url parameter to be sent back + /// in negative assertions in response to OpenID 1.x RP's checkid_immediate requests. + /// </summary> + /// <param name="immediateRequest">The immediate request.</param> + /// <param name="channel">The channel to use to simulate construction of the message.</param> + /// <returns>The value to use for the user_setup_url parameter.</returns> + private static Uri ConstructUserSetupUrl(CheckIdRequest immediateRequest, Channel channel) { + Contract.Requires<ArgumentNullException>(immediateRequest != null); + Contract.Requires<ArgumentNullException>(channel != null); + ErrorUtilities.VerifyInternal(immediateRequest.Immediate, "Only immediate requests should be sent here."); + + var setupRequest = new CheckIdRequest(immediateRequest.Version, immediateRequest.Recipient, AuthenticationRequestMode.Setup); + setupRequest.LocalIdentifier = immediateRequest.LocalIdentifier; + setupRequest.ReturnTo = immediateRequest.ReturnTo; + setupRequest.Realm = immediateRequest.Realm; + setupRequest.AssociationHandle = immediateRequest.AssociationHandle; + return channel.PrepareResponse(setupRequest).GetDirectUriRequest(channel); + } + + /// <summary> + /// Gets the value for the openid.mode that is appropriate for this response. + /// </summary> + /// <param name="request">The request that we're responding to.</param> + /// <returns>The value of the openid.mode parameter to use.</returns> + private static string GetMode(SignedResponseRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + Protocol protocol = Protocol.Lookup(request.Version); + return request.Immediate ? protocol.Args.Mode.setup_needed : protocol.Args.Mode.cancel; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/PositiveAssertionResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/PositiveAssertionResponse.cs new file mode 100644 index 0000000..006ea93 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/PositiveAssertionResponse.cs @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------- +// <copyright file="PositiveAssertionResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Net.Security; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + + /// <summary> + /// An identity assertion from a Provider to a Relying Party, stating that the + /// user operating the user agent is in fact some specific user known to the Provider. + /// </summary> + [DebuggerDisplay("OpenID {Version} {Mode} {LocalIdentifier}")] + [Serializable] + internal class PositiveAssertionResponse : IndirectSignedResponse { + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAssertionResponse"/> class. + /// </summary> + /// <param name="request"> + /// The authentication request that caused this assertion to be generated. + /// </param> + internal PositiveAssertionResponse(CheckIdRequest request) + : base(request) { + this.ClaimedIdentifier = request.ClaimedIdentifier; + this.LocalIdentifier = request.LocalIdentifier; + } + + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAssertionResponse"/> class + /// for unsolicited assertions. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="relyingPartyReturnTo">The return_to URL of the Relying Party. + /// This value will commonly be from <see cref="SignedResponseRequest.ReturnTo"/>, + /// but for unsolicited assertions may come from the Provider performing RP discovery + /// to find the appropriate return_to URL to use.</param> + internal PositiveAssertionResponse(Version version, Uri relyingPartyReturnTo) + : base(version, relyingPartyReturnTo) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="PositiveAssertionResponse"/> class. + /// </summary> + /// <param name="relyingParty">The relying party return_to endpoint that will receive this positive assertion.</param> + internal PositiveAssertionResponse(RelyingPartyEndpointDescription relyingParty) + : this(relyingParty.Protocol.Version, relyingParty.ReturnToEndpoint) { + } + + /// <summary> + /// Gets or sets the Claimed Identifier. + /// </summary> + /// <remarks> + /// <para>"openid.claimed_id" and "openid.identity" SHALL be either both present or both absent. + /// If neither value is present, the assertion is not about an identifier, + /// and will contain other information in its payload, using extensions (Extensions). </para> + /// </remarks> + [MessagePart("openid.claimed_id", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign, MinVersion = "2.0")] + internal Identifier ClaimedIdentifier { get; set; } + + /// <summary> + /// Gets or sets the OP Local Identifier. + /// </summary> + /// <value>The OP-Local Identifier. </value> + /// <remarks> + /// <para>OpenID Providers MAY assist the end user in selecting the Claimed + /// and OP-Local Identifiers about which the assertion is made. + /// The openid.identity field MAY be omitted if an extension is in use that + /// makes the response meaningful without it (see openid.claimed_id above). </para> + /// </remarks> + [MessagePart("openid.identity", IsRequired = true, AllowEmpty = false, RequiredProtection = ProtectionLevel.Sign)] + internal Identifier LocalIdentifier { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/RequestBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/RequestBase.cs new file mode 100644 index 0000000..8e4cb9d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/RequestBase.cs @@ -0,0 +1,185 @@ +//----------------------------------------------------------------------- +// <copyright file="RequestBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A common base class for OpenID request messages and indirect responses (since they are ultimately requests). + /// </summary> + [DebuggerDisplay("OpenID {Version} {Mode}")] + [Serializable] + internal class RequestBase : IDirectedProtocolMessage { + /// <summary> + /// The openid.ns parameter in the message. + /// </summary> + /// <value>"http://specs.openid.net/auth/2.0" </value> + /// <remarks> + /// This particular value MUST be present for the request to be a valid OpenID Authentication 2.0 request. Future versions of the specification may define different values in order to allow message recipients to properly interpret the request. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Read by reflection.")] + [MessagePart("openid.ns", IsRequired = true, AllowEmpty = false, MinVersion = "2.0")] +#pragma warning disable 0414 // read by reflection + private readonly string OpenIdNamespace = Protocol.OpenId2Namespace; +#pragma warning restore 0414 + + /// <summary> + /// Backing store for the <see cref="ExtraData"/> property. + /// </summary> + private readonly Dictionary<string, string> extraData = new Dictionary<string, string>(); + + /// <summary> + /// Backing store for the <see cref="Incoming"/> property. + /// </summary> + private bool incoming; + + /// <summary> + /// Initializes a new instance of the <see cref="RequestBase"/> class. + /// </summary> + /// <param name="version">The OpenID version this message must comply with.</param> + /// <param name="providerEndpoint">The OpenID Provider endpoint.</param> + /// <param name="mode">The value for the openid.mode parameter.</param> + /// <param name="transport">A value indicating whether the message will be transmitted directly or indirectly.</param> + protected RequestBase(Version version, Uri providerEndpoint, string mode, MessageTransport transport) { + Contract.Requires<ArgumentNullException>(providerEndpoint != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(mode)); + + this.Recipient = providerEndpoint; + this.Mode = mode; + this.Transport = transport; + this.Version = version; + } + + /// <summary> + /// Gets the value of the openid.mode parameter. + /// </summary> + [MessagePart("openid.mode", IsRequired = true, AllowEmpty = false)] + public string Mode { get; private set; } + + #region IDirectedProtocolMessage Members + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + /// <value> + /// For direct messages this is the OpenID mandated POST. + /// For indirect messages both GET and POST are allowed. + /// </value> + HttpDeliveryMethods IDirectedProtocolMessage.HttpMethods { + get { + // OpenID 2.0 section 5.1.1 + HttpDeliveryMethods methods = HttpDeliveryMethods.PostRequest; + if (this.Transport == MessageTransport.Indirect) { + methods |= HttpDeliveryMethods.GetRequest; + } + return methods; + } + } + + /// <summary> + /// Gets the recipient of the message. + /// </summary> + /// <value>The OP endpoint, or the RP return_to.</value> + public Uri Recipient { + get; + private set; + } + + #endregion + + #region IProtocolMessage Properties + + /// <summary> + /// Gets the version of the protocol this message is prepared to implement. + /// </summary> + /// <value>Version 2.0</value> + public Version Version { get; private set; } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + /// <value><see cref="MessageProtections.None"/></value> + public virtual MessageProtections RequiredProtection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + /// <value><see cref="MessageTransport.Direct"/></value> + public MessageTransport Transport { get; private set; } + + /// <summary> + /// Gets the extra parameters included in the message. + /// </summary> + /// <value>An empty dictionary.</value> + public IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + #endregion + + /// <summary> + /// Gets a value indicating whether this message was deserialized as an incoming message. + /// </summary> + protected internal bool Incoming { + get { return this.incoming; } + } + + /// <summary> + /// Gets the protocol used by this message. + /// </summary> + protected Protocol Protocol { + get { return Protocol.Lookup(this.Version); } + } + + #region IProtocolMessage Methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public virtual void EnsureValidMessage() { + } + + #endregion + + /// <summary> + /// Sets a flag indicating that this message is received (as opposed to sent). + /// </summary> + internal void SetAsIncoming() { + this.incoming = true; + } + + /// <summary> + /// Gets some string from a given version of the OpenID protocol. + /// </summary> + /// <param name="protocolVersion">The protocol version to use for lookup.</param> + /// <param name="mode">A function that can retrieve the desired protocol constant.</param> + /// <returns>The value of the constant.</returns> + /// <remarks> + /// This method can be used by a constructor to throw an <see cref="ArgumentNullException"/> + /// instead of a <see cref="NullReferenceException"/>. + /// </remarks> + protected static string GetProtocolConstant(Version protocolVersion, Func<Protocol, string> mode) { + Contract.Requires<ArgumentNullException>(protocolVersion != null); + return mode(Protocol.Lookup(protocolVersion)); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Messages/SignedResponseRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Messages/SignedResponseRequest.cs new file mode 100644 index 0000000..7eb5407 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Messages/SignedResponseRequest.cs @@ -0,0 +1,185 @@ +//----------------------------------------------------------------------- +// <copyright file="SignedResponseRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// An indirect request from a Relying Party to a Provider where the response + /// is expected to be signed. + /// </summary> + [Serializable] + internal class SignedResponseRequest : RequestBase, IProtocolMessageWithExtensions { + /// <summary> + /// Backing store for the <see cref="Extensions"/> property. + /// </summary> + private IList<IExtensionMessage> extensions = new List<IExtensionMessage>(); + + /// <summary> + /// Initializes a new instance of the <see cref="SignedResponseRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="providerEndpoint">The Provider endpoint that receives this message.</param> + /// <param name="mode"> + /// <see cref="AuthenticationRequestMode.Immediate"/> for asynchronous javascript clients; + /// <see cref="AuthenticationRequestMode.Setup"/> to allow the Provider to interact with the user in order to complete authentication. + /// </param> + internal SignedResponseRequest(Version version, Uri providerEndpoint, AuthenticationRequestMode mode) : + base(version, providerEndpoint, GetMode(version, mode), DotNetOpenAuth.Messaging.MessageTransport.Indirect) { + } + + #region IProtocolMessageWithExtensions Members + + /// <summary> + /// Gets the list of extensions that are included with this message. + /// </summary> + /// <value></value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + public IList<IExtensionMessage> Extensions { + get { return this.extensions; } + } + + #endregion + + /// <summary> + /// Gets a value indicating whether the Provider is allowed to interact with the user + /// as part of authentication. + /// </summary> + /// <value><c>true</c> if using OpenID immediate mode; otherwise, <c>false</c>.</value> + internal bool Immediate { + get { return String.Equals(this.Mode, Protocol.Args.Mode.checkid_immediate, StringComparison.Ordinal); } + } + + /// <summary> + /// Gets or sets the handle of the association the RP would like the Provider + /// to use for signing a positive assertion in the response message. + /// </summary> + /// <value>A handle for an association between the Relying Party and the OP + /// that SHOULD be used to sign the response. </value> + /// <remarks> + /// If no association handle is sent, the transaction will take place in Stateless Mode + /// (Verifying Directly with the OpenID Provider). + /// </remarks> + [MessagePart("openid.assoc_handle", IsRequired = false, AllowEmpty = false)] + internal string AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the URL the Provider should redirect the user agent to following + /// the authentication attempt. + /// </summary> + /// <value>URL to which the OP SHOULD return the User-Agent with the response + /// indicating the status of the request.</value> + /// <remarks> + /// <para>If this value is not sent in the request it signifies that the Relying Party + /// does not wish for the end user to be returned. </para> + /// <para>The return_to URL MAY be used as a mechanism for the Relying Party to attach + /// context about the authentication request to the authentication response. + /// This document does not define a mechanism by which the RP can ensure that query + /// parameters are not modified by outside parties; such a mechanism can be defined + /// by the RP itself. </para> + /// </remarks> + [MessagePart("openid.return_to", IsRequired = true, AllowEmpty = false)] + [MessagePart("openid.return_to", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + internal Uri ReturnTo { get; set; } + + /// <summary> + /// Gets or sets the Relying Party discovery URL the Provider may use to verify the + /// source of the authentication request. + /// </summary> + /// <value> + /// URL pattern the OP SHOULD ask the end user to trust. See Section 9.2 (Realms). + /// This value MUST be sent if openid.return_to is omitted. + /// Default: The <see cref="ReturnTo"/> URL. + /// </value> + [MessagePart("openid.trust_root", IsRequired = false, AllowEmpty = false)] + [MessagePart("openid.realm", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + internal Realm Realm { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the return_to value should be signed. + /// </summary> + internal bool SignReturnTo { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.Realm == null) { + // Set the default Realm per the spec if it is not explicitly given. + this.Realm = this.ReturnTo; + } else if (this.ReturnTo != null) { + // Verify that the realm and return_to agree. + ErrorUtilities.VerifyProtocol(this.Realm.Contains(this.ReturnTo), OpenIdStrings.ReturnToNotUnderRealm, this.ReturnTo, this.Realm); + } + } + + /// <summary> + /// Adds parameters to the return_to querystring. + /// </summary> + /// <param name="keysValues">The keys=value pairs to add to the return_to query string.</param> + /// <remarks> + /// This method is useful if the Relying Party wants to recall some value + /// when and if a positive assertion comes back from the Provider. + /// </remarks> + internal void AddReturnToArguments(IEnumerable<KeyValuePair<string, string>> keysValues) { + Contract.Requires<ArgumentNullException>(keysValues != null); + ErrorUtilities.VerifyOperation(this.ReturnTo != null, OpenIdStrings.ReturnToRequiredForOperation); + UriBuilder returnToBuilder = new UriBuilder(this.ReturnTo); + returnToBuilder.AppendAndReplaceQueryArgs(keysValues); + this.ReturnTo = returnToBuilder.Uri; + } + + /// <summary> + /// Adds a parameter to the return_to querystring. + /// </summary> + /// <param name="key">The name of the parameter.</param> + /// <param name="value">The value of the argument.</param> + /// <remarks> + /// This method is useful if the Relying Party wants to recall some value + /// when and if a positive assertion comes back from the Provider. + /// </remarks> + internal void AddReturnToArguments(string key, string value) { + var pair = new KeyValuePair<string, string>(key, value); + this.AddReturnToArguments(new[] { pair }); + } + + /// <summary> + /// Gets the value of the openid.mode parameter based on the protocol version and immediate flag. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="mode"> + /// <see cref="AuthenticationRequestMode.Immediate"/> for asynchronous javascript clients; + /// <see cref="AuthenticationRequestMode.Setup"/> to allow the Provider to interact with the user in order to complete authentication. + /// </param> + /// <returns>checkid_immediate or checkid_setup</returns> + private static string GetMode(Version version, AuthenticationRequestMode mode) { + Contract.Requires<ArgumentNullException>(version != null); + + Protocol protocol = Protocol.Lookup(version); + return mode == AuthenticationRequestMode.Immediate ? protocol.Args.Mode.checkid_immediate : protocol.Args.Mode.checkid_setup; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/NoDiscoveryIdentifier.cs new file mode 100644 index 0000000..1a6e7e9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/NoDiscoveryIdentifier.cs @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------- +// <copyright file="NoDiscoveryIdentifier.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; + + /// <summary> + /// Wraps an existing Identifier and prevents it from performing discovery. + /// </summary> + [ContractVerification(true)] + [Pure] + internal class NoDiscoveryIdentifier : Identifier { + /// <summary> + /// The wrapped identifier. + /// </summary> + private readonly Identifier wrappedIdentifier; + + /// <summary> + /// Initializes a new instance of the <see cref="NoDiscoveryIdentifier"/> class. + /// </summary> + /// <param name="wrappedIdentifier">The ordinary Identifier whose discovery is being masked.</param> + /// <param name="claimSsl">Whether this Identifier should claim to be SSL-secure, although no discovery will never generate service endpoints anyway.</param> + internal NoDiscoveryIdentifier(Identifier wrappedIdentifier, bool claimSsl) + : base(wrappedIdentifier.OriginalString, claimSsl) { + Contract.Requires<ArgumentNullException>(wrappedIdentifier != null); + + this.wrappedIdentifier = wrappedIdentifier; + } + + /// <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() { + return this.wrappedIdentifier.ToString(); + } + + /// <summary> + /// Tests equality between two <see cref="Identifier"/>s. + /// </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) { + return this.wrappedIdentifier.Equals(obj); + } + + /// <summary> + /// Gets the hash code for an <see cref="Identifier"/> for storage in a hashtable. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return this.wrappedIdentifier.GetHashCode(); + } + + /// <summary> + /// Returns an <see cref="Identifier"/> that has no URI fragment. + /// Quietly returns the original <see cref="Identifier"/> if it is not + /// a <see cref="UriIdentifier"/> or no fragment exists. + /// </summary> + /// <returns> + /// A new <see cref="Identifier"/> instance if there was a + /// fragment to remove, otherwise this same instance.. + /// </returns> + internal override Identifier TrimFragment() { + return new NoDiscoveryIdentifier(this.wrappedIdentifier.TrimFragment(), IsDiscoverySecureEndToEnd); + } + + /// <summary> + /// Converts a given identifier to its secure equivalent. + /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS. + /// Discovery is made to require SSL for the entire resolution process. + /// </summary> + /// <param name="secureIdentifier">The newly created secure identifier. + /// If the conversion fails, <paramref name="secureIdentifier"/> retains + /// <i>this</i> identifiers identity, but will never discover any endpoints.</param> + /// <returns> + /// True if the secure conversion was successful. + /// False if the Identifier was originally created with an explicit HTTP scheme. + /// </returns> + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + return this.wrappedIdentifier.TryRequireSsl(out secureIdentifier); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.Designer.cs new file mode 100644 index 0000000..f45af93 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.Designer.cs @@ -0,0 +1,823 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.225 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.OpenId { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class OpenIdStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal OpenIdStrings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.OpenId.OpenIdStrings", typeof(OpenIdStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to An absolute URI is required for this value.. + /// </summary> + internal static string AbsoluteUriRequired { + get { + return ResourceManager.GetString("AbsoluteUriRequired", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This is already a PPID Identifier.. + /// </summary> + internal static string ArgumentIsPpidIdentifier { + get { + return ResourceManager.GetString("ArgumentIsPpidIdentifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The requested association type '{0}' with session type '{1}' is unrecognized or not supported by this Provider due to security requirements.. + /// </summary> + internal static string AssociationOrSessionTypeUnrecognizedOrNotSupported { + get { + return ResourceManager.GetString("AssociationOrSessionTypeUnrecognizedOrNotSupported", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The length of the shared secret ({0}) does not match the length required by the association type ('{1}').. + /// </summary> + internal static string AssociationSecretAndTypeLengthMismatch { + get { + return ResourceManager.GetString("AssociationSecretAndTypeLengthMismatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The length of the encrypted shared secret ({0}) does not match the length of the hashing algorithm ({1}).. + /// </summary> + internal static string AssociationSecretHashLengthMismatch { + get { + return ResourceManager.GetString("AssociationSecretHashLengthMismatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No association store has been given but is required for the current configuration.. + /// </summary> + internal static string AssociationStoreRequired { + get { + return ResourceManager.GetString("AssociationStoreRequired", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to If an association store is given, a nonce store must also be provided.. + /// </summary> + internal static string AssociationStoreRequiresNonceStore { + get { + return ResourceManager.GetString("AssociationStoreRequiresNonceStore", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An attribute with type URI '{0}' has already been added.. + /// </summary> + internal static string AttributeAlreadyAdded { + get { + return ResourceManager.GetString("AttributeAlreadyAdded", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Only {0} values for attribute '{1}' were requested, but {2} were supplied.. + /// </summary> + internal static string AttributeTooManyValues { + get { + return ResourceManager.GetString("AttributeTooManyValues", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.. + /// </summary> + internal static string BadAssociationPrivateData { + get { + return ResourceManager.GetString("BadAssociationPrivateData", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The {0} extension failed to deserialize and will be skipped. {1}. + /// </summary> + internal static string BadExtension { + get { + return ResourceManager.GetString("BadExtension", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Callback arguments are only supported when a {0} is provided to the {1}.. + /// </summary> + internal static string CallbackArgumentsRequireSecretStore { + get { + return ResourceManager.GetString("CallbackArgumentsRequireSecretStore", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A Simple Registration request can only generate a response on the receiving end.. + /// </summary> + internal static string CallDeserializeBeforeCreateResponse { + get { + return ResourceManager.GetString("CallDeserializeBeforeCreateResponse", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The openid.claimed_id and openid.identity parameters must both be present or both be absent.. + /// </summary> + internal static string ClaimedIdAndLocalIdMustBothPresentOrAbsent { + get { + return ResourceManager.GetString("ClaimedIdAndLocalIdMustBothPresentOrAbsent", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The ClaimedIdentifier property cannot be set when IsDelegatedIdentifier is true to avoid breaking OpenID URL delegation.. + /// </summary> + internal static string ClaimedIdentifierCannotBeSetOnDelegatedAuthentication { + get { + return ResourceManager.GetString("ClaimedIdentifierCannotBeSetOnDelegatedAuthentication", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This OpenID exploits features that this relying party cannot reliably verify. Please try logging in with a human-readable OpenID or from a different OpenID Provider.. + /// </summary> + internal static string ClaimedIdentifierDefiesDotNetNormalization { + get { + return ResourceManager.GetString("ClaimedIdentifierDefiesDotNetNormalization", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The ClaimedIdentifier property must be set first.. + /// </summary> + internal static string ClaimedIdentifierMustBeSetFirst { + get { + return ResourceManager.GetString("ClaimedIdentifierMustBeSetFirst", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An extension with this property name ('{0}') has already been registered.. + /// </summary> + internal static string ClientScriptExtensionPropertyNameCollision { + get { + return ResourceManager.GetString("ClientScriptExtensionPropertyNameCollision", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The extension '{0}' has already been registered.. + /// </summary> + internal static string ClientScriptExtensionTypeCollision { + get { + return ResourceManager.GetString("ClientScriptExtensionTypeCollision", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An authentication request has already been created using CreateRequest().. + /// </summary> + internal static string CreateRequestAlreadyCalled { + get { + return ResourceManager.GetString("CreateRequestAlreadyCalled", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Only OpenIDs issued directly by their OpenID Provider are allowed here.. + /// </summary> + internal static string DelegatingIdentifiersNotAllowed { + get { + return ResourceManager.GetString("DelegatingIdentifiersNotAllowed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The associate request instance must be a Diffie-Hellman instance.. + /// </summary> + internal static string DiffieHellmanAssociationRequired { + get { + return ResourceManager.GetString("DiffieHellmanAssociationRequired", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}. + /// </summary> + internal static string DiffieHellmanRequiredPropertiesNotSet { + get { + return ResourceManager.GetString("DiffieHellmanRequiredPropertiesNotSet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to URI is not SSL yet requireSslDiscovery is set to true.. + /// </summary> + internal static string ExplicitHttpUriSuppliedWithSslRequirement { + get { + return ResourceManager.GetString("ExplicitHttpUriSuppliedWithSslRequirement", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An extension sharing namespace '{0}' has already been added. Only one extension per namespace is allowed in a given request.. + /// </summary> + internal static string ExtensionAlreadyAddedWithSameTypeURI { + get { + return ResourceManager.GetString("ExtensionAlreadyAddedWithSameTypeURI", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot lookup extension support on a rehydrated ServiceEndpoint.. + /// </summary> + internal static string ExtensionLookupSupportUnavailable { + get { + return ResourceManager.GetString("ExtensionLookupSupportUnavailable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Fragment segments do not apply to XRI identifiers.. + /// </summary> + internal static string FragmentNotAllowedOnXRIs { + get { + return ResourceManager.GetString("FragmentNotAllowedOnXRIs", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The HTML head tag must include runat="server".. + /// </summary> + internal static string HeadTagMustIncludeRunatServer { + get { + return ResourceManager.GetString("HeadTagMustIncludeRunatServer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to ClaimedIdentifier and LocalIdentifier must be the same when IsIdentifierSelect is true.. + /// </summary> + internal static string IdentifierSelectRequiresMatchingIdentifiers { + get { + return ResourceManager.GetString("IdentifierSelectRequiresMatchingIdentifiers", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The openid.identity and openid.claimed_id parameters must either be both present or both absent from the message.. + /// </summary> + internal static string IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent { + get { + return ResourceManager.GetString("IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The Provider requested association type '{0}' and session type '{1}', which are not compatible with each other.. + /// </summary> + internal static string IncompatibleAssociationAndSessionTypes { + get { + return ResourceManager.GetString("IncompatibleAssociationAndSessionTypes", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to {0} (Contact: {1}, Reference: {2}). + /// </summary> + internal static string IndirectErrorFormattedMessage { + get { + return ResourceManager.GetString("IndirectErrorFormattedMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}'). + /// </summary> + internal static string InvalidCharacterInKeyValueFormInput { + get { + return ResourceManager.GetString("InvalidCharacterInKeyValueFormInput", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Invalid XmlDSig signature on XRDS document.. + /// </summary> + internal static string InvalidDSig { + get { + return ResourceManager.GetString("InvalidDSig", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot decode Key-Value Form because a line was found without a '{0}' character. (line {1}: '{2}'). + /// </summary> + internal static string InvalidKeyValueFormCharacterMissing { + get { + return ResourceManager.GetString("InvalidKeyValueFormCharacterMissing", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The scheme must be http or https but was '{0}'.. + /// </summary> + internal static string InvalidScheme { + get { + return ResourceManager.GetString("InvalidScheme", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The value '{0}' is not a valid URI.. + /// </summary> + internal static string InvalidUri { + get { + return ResourceManager.GetString("InvalidUri", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Not a recognized XRI format.. + /// </summary> + internal static string InvalidXri { + get { + return ResourceManager.GetString("InvalidXri", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The OpenID Provider issued an assertion for an Identifier whose discovery information did not match. + ///Assertion endpoint info: + ///{0} + ///Discovered endpoint info: + ///{1}. + /// </summary> + internal static string IssuedAssertionFailsIdentifierDiscovery { + get { + return ResourceManager.GetString("IssuedAssertionFailsIdentifierDiscovery", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The list of keys do not match the provided dictionary.. + /// </summary> + internal static string KeysListAndDictionaryDoNotMatch { + get { + return ResourceManager.GetString("KeysListAndDictionaryDoNotMatch", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The '{0}' and '{1}' parameters must both be or not be '{2}'.. + /// </summary> + internal static string MatchingArgumentsExpected { + get { + return ResourceManager.GetString("MatchingArgumentsExpected", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The maximum time allowed to complete authentication has been exceeded. Please try again.. + /// </summary> + internal static string MaximumAuthenticationTimeExpired { + get { + return ResourceManager.GetString("MaximumAuthenticationTimeExpired", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to X.509 signing certificate issued to {0}, but a certificate for {1} was expected.. + /// </summary> + internal static string MisdirectedSigningCertificate { + get { + return ResourceManager.GetString("MisdirectedSigningCertificate", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Missing {0} element.. + /// </summary> + internal static string MissingElement { + get { + return ResourceManager.GetString("MissingElement", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No recognized association type matches the requested length of {0}.. + /// </summary> + internal static string NoAssociationTypeFoundByLength { + get { + return ResourceManager.GetString("NoAssociationTypeFoundByLength", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No recognized association type matches the requested name of '{0}'.. + /// </summary> + internal static string NoAssociationTypeFoundByName { + get { + return ResourceManager.GetString("NoAssociationTypeFoundByName", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unless using transport layer encryption, "no-encryption" MUST NOT be used.. + /// </summary> + internal static string NoEncryptionSessionRequiresHttps { + get { + return ResourceManager.GetString("NoEncryptionSessionRequiresHttps", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No identifier has been set.. + /// </summary> + internal static string NoIdentifierSet { + get { + return ResourceManager.GetString("NoIdentifierSet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No XRDS document containing OpenID relying party endpoint information could be found at {0}.. + /// </summary> + internal static string NoRelyingPartyEndpointDiscovered { + get { + return ResourceManager.GetString("NoRelyingPartyEndpointDiscovered", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Diffie-Hellman session type '{0}' not found for OpenID {1}.. + /// </summary> + internal static string NoSessionTypeFound { + get { + return ResourceManager.GetString("NoSessionTypeFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This operation is not supported by serialized authentication responses. Try this operation from the LoggedIn event handler.. + /// </summary> + internal static string NotSupportedByAuthenticationSnapshot { + get { + return ResourceManager.GetString("NotSupportedByAuthenticationSnapshot", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No OpenID endpoint found.. + /// </summary> + internal static string OpenIdEndpointNotFound { + get { + return ResourceManager.GetString("OpenIdEndpointNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to No OpenID url is provided.. + /// </summary> + internal static string OpenIdTextBoxEmpty { + get { + return ResourceManager.GetString("OpenIdTextBoxEmpty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This operation is only allowed when IAuthenticationResponse.State == AuthenticationStatus.SetupRequired.. + /// </summary> + internal static string OperationOnlyValidForSetupRequiredState { + get { + return ResourceManager.GetString("OperationOnlyValidForSetupRequiredState", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to OpenID popup window or iframe did not recognize an OpenID response in the request.. + /// </summary> + internal static string PopupRedirectMissingResponse { + get { + return ResourceManager.GetString("PopupRedirectMissingResponse", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An positive OpenID assertion was received from OP endpoint {0} and was rejected based on this site's security settings.. + /// </summary> + internal static string PositiveAssertionFromNonQualifiedProvider { + get { + return ResourceManager.GetString("PositiveAssertionFromNonQualifiedProvider", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unable to find the signing secret by the handle '{0}'.. + /// </summary> + internal static string PrivateRPSecretNotFound { + get { + return ResourceManager.GetString("PrivateRPSecretNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The {0} property must be set first.. + /// </summary> + internal static string PropertyNotSet { + get { + return ResourceManager.GetString("PropertyNotSet", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This property value is not supported by this control.. + /// </summary> + internal static string PropertyValueNotSupported { + get { + return ResourceManager.GetString("PropertyValueNotSupported", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unable to determine the version of the OpenID protocol implemented by the Provider at endpoint '{0}'.. + /// </summary> + internal static string ProviderVersionUnrecognized { + get { + return ResourceManager.GetString("ProviderVersionUnrecognized", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An HTTP request to the realm URL ({0}) resulted in a redirect, which is not allowed during relying party discovery.. + /// </summary> + internal static string RealmCausedRedirectUponDiscovery { + get { + return ResourceManager.GetString("RealmCausedRedirectUponDiscovery", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Sorry. This site only accepts OpenIDs that are HTTPS-secured, but {0} is not a secure Identifier.. + /// </summary> + internal static string RequireSslNotSatisfiedByAssertedClaimedId { + get { + return ResourceManager.GetString("RequireSslNotSatisfiedByAssertedClaimedId", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The response is not ready. Use IsResponseReady to check whether a response is ready first.. + /// </summary> + internal static string ResponseNotReady { + get { + return ResourceManager.GetString("ResponseNotReady", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to return_to '{0}' not under realm '{1}'.. + /// </summary> + internal static string ReturnToNotUnderRealm { + get { + return ResourceManager.GetString("ReturnToNotUnderRealm", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The {0} parameter ({1}) does not match the actual URL ({2}) the request was made with.. + /// </summary> + internal static string ReturnToParamDoesNotMatchRequestUrl { + get { + return ResourceManager.GetString("ReturnToParamDoesNotMatchRequestUrl", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The ReturnTo property must not be null to support this operation.. + /// </summary> + internal static string ReturnToRequiredForOperation { + get { + return ResourceManager.GetString("ReturnToRequiredForOperation", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The openid.return_to parameter is required in the request message in order to construct a response, but that parameter was missing.. + /// </summary> + internal static string ReturnToRequiredForResponse { + get { + return ResourceManager.GetString("ReturnToRequiredForResponse", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The following parameter(s) are not included in the signature but must be: {0}. + /// </summary> + internal static string SignatureDoesNotIncludeMandatoryParts { + get { + return ResourceManager.GetString("SignatureDoesNotIncludeMandatoryParts", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Invalid birthdate value. Must be in the form yyyy-MM-dd.. + /// </summary> + internal static string SregInvalidBirthdate { + get { + return ResourceManager.GetString("SregInvalidBirthdate", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type must implement {0}.. + /// </summary> + internal static string TypeMustImplementX { + get { + return ResourceManager.GetString("TypeMustImplementX", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The property {0} had unexpected value {1}.. + /// </summary> + internal static string UnexpectedEnumPropertyValue { + get { + return ResourceManager.GetString("UnexpectedEnumPropertyValue", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unexpected HTTP status code {0} {1} received in direct response.. + /// </summary> + internal static string UnexpectedHttpStatusCode { + get { + return ResourceManager.GetString("UnexpectedHttpStatusCode", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An unsolicited assertion cannot be sent for the claimed identifier {0} because this is not an authorized Provider for that identifier.. + /// </summary> + internal static string UnsolicitedAssertionForUnrelatedClaimedIdentifier { + get { + return ResourceManager.GetString("UnsolicitedAssertionForUnrelatedClaimedIdentifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Rejecting unsolicited assertions requires a nonce store and an association store.. + /// </summary> + internal static string UnsolicitedAssertionRejectionRequiresNonceStore { + get { + return ResourceManager.GetString("UnsolicitedAssertionRejectionRequiresNonceStore", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unsolicited assertions are not allowed at this relying party.. + /// </summary> + internal static string UnsolicitedAssertionsNotAllowed { + get { + return ResourceManager.GetString("UnsolicitedAssertionsNotAllowed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unsolicited assertions are not allowed from 1.0 OpenID Providers.. + /// </summary> + internal static string UnsolicitedAssertionsNotAllowedFrom1xOPs { + get { + return ResourceManager.GetString("UnsolicitedAssertionsNotAllowedFrom1xOPs", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Providing a DateTime whose Kind is Unspecified is not allowed.. + /// </summary> + internal static string UnspecifiedDateTimeKindNotAllowed { + get { + return ResourceManager.GetString("UnspecifiedDateTimeKindNotAllowed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unrecognized or missing canonicalization method.. + /// </summary> + internal static string UnsupportedCanonicalizationMethod { + get { + return ResourceManager.GetString("UnsupportedCanonicalizationMethod", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This feature is unavailable due to an unrecognized channel configuration.. + /// </summary> + internal static string UnsupportedChannelConfiguration { + get { + return ResourceManager.GetString("UnsupportedChannelConfiguration", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unrecognized or missing signature method.. + /// </summary> + internal static string UnsupportedSignatureMethod { + get { + return ResourceManager.GetString("UnsupportedSignatureMethod", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The openid.user_setup_url parameter is required when sending negative assertion messages in response to immediate mode requests.. + /// </summary> + internal static string UserSetupUrlRequiredInImmediateNegativeResponse { + get { + return ResourceManager.GetString("UserSetupUrlRequiredInImmediateNegativeResponse", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The X.509 certificate used to sign this document is not trusted.. + /// </summary> + internal static string X509CertificateNotTrusted { + get { + return ResourceManager.GetString("X509CertificateNotTrusted", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to XRI support has been disabled at this site.. + /// </summary> + internal static string XriResolutionDisabled { + get { + return ResourceManager.GetString("XriResolutionDisabled", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to XRI resolution failed.. + /// </summary> + internal static string XriResolutionFailed { + get { + return ResourceManager.GetString("XriResolutionFailed", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.resx new file mode 100644 index 0000000..b700d76 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.resx @@ -0,0 +1,376 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AssociationSecretAndTypeLengthMismatch" xml:space="preserve"> + <value>The length of the shared secret ({0}) does not match the length required by the association type ('{1}').</value> + </data> + <data name="AssociationSecretHashLengthMismatch" xml:space="preserve"> + <value>The length of the encrypted shared secret ({0}) does not match the length of the hashing algorithm ({1}).</value> + </data> + <data name="AssociationStoreRequiresNonceStore" xml:space="preserve"> + <value>If an association store is given, a nonce store must also be provided.</value> + </data> + <data name="BadAssociationPrivateData" xml:space="preserve"> + <value>The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.</value> + </data> + <data name="CallbackArgumentsRequireSecretStore" xml:space="preserve"> + <value>Callback arguments are only supported when a {0} is provided to the {1}.</value> + </data> + <data name="CallDeserializeBeforeCreateResponse" xml:space="preserve"> + <value>A Simple Registration request can only generate a response on the receiving end.</value> + </data> + <data name="ClaimedIdAndLocalIdMustBothPresentOrAbsent" xml:space="preserve"> + <value>The openid.claimed_id and openid.identity parameters must both be present or both be absent.</value> + </data> + <data name="ClaimedIdentifierCannotBeSetOnDelegatedAuthentication" xml:space="preserve"> + <value>The ClaimedIdentifier property cannot be set when IsDelegatedIdentifier is true to avoid breaking OpenID URL delegation.</value> + </data> + <data name="ClaimedIdentifierMustBeSetFirst" xml:space="preserve"> + <value>The ClaimedIdentifier property must be set first.</value> + </data> + <data name="DiffieHellmanRequiredPropertiesNotSet" xml:space="preserve"> + <value>The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}</value> + </data> + <data name="ExplicitHttpUriSuppliedWithSslRequirement" xml:space="preserve"> + <value>URI is not SSL yet requireSslDiscovery is set to true.</value> + </data> + <data name="ExtensionAlreadyAddedWithSameTypeURI" xml:space="preserve"> + <value>An extension sharing namespace '{0}' has already been added. Only one extension per namespace is allowed in a given request.</value> + </data> + <data name="ExtensionLookupSupportUnavailable" xml:space="preserve"> + <value>Cannot lookup extension support on a rehydrated ServiceEndpoint.</value> + </data> + <data name="FragmentNotAllowedOnXRIs" xml:space="preserve"> + <value>Fragment segments do not apply to XRI identifiers.</value> + </data> + <data name="IdentifierSelectRequiresMatchingIdentifiers" xml:space="preserve"> + <value>ClaimedIdentifier and LocalIdentifier must be the same when IsIdentifierSelect is true.</value> + </data> + <data name="IndirectErrorFormattedMessage" xml:space="preserve"> + <value>{0} (Contact: {1}, Reference: {2})</value> + </data> + <data name="InvalidCharacterInKeyValueFormInput" xml:space="preserve"> + <value>Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}')</value> + </data> + <data name="InvalidKeyValueFormCharacterMissing" xml:space="preserve"> + <value>Cannot decode Key-Value Form because a line was found without a '{0}' character. (line {1}: '{2}')</value> + </data> + <data name="InvalidScheme" xml:space="preserve"> + <value>The scheme must be http or https but was '{0}'.</value> + </data> + <data name="InvalidUri" xml:space="preserve"> + <value>The value '{0}' is not a valid URI.</value> + </data> + <data name="InvalidXri" xml:space="preserve"> + <value>Not a recognized XRI format.</value> + </data> + <data name="IssuedAssertionFailsIdentifierDiscovery" xml:space="preserve"> + <value>The OpenID Provider issued an assertion for an Identifier whose discovery information did not match. +Assertion endpoint info: +{0} +Discovered endpoint info: +{1}</value> + </data> + <data name="KeysListAndDictionaryDoNotMatch" xml:space="preserve"> + <value>The list of keys do not match the provided dictionary.</value> + </data> + <data name="MatchingArgumentsExpected" xml:space="preserve"> + <value>The '{0}' and '{1}' parameters must both be or not be '{2}'.</value> + </data> + <data name="NoAssociationTypeFoundByLength" xml:space="preserve"> + <value>No recognized association type matches the requested length of {0}.</value> + </data> + <data name="NoAssociationTypeFoundByName" xml:space="preserve"> + <value>No recognized association type matches the requested name of '{0}'.</value> + </data> + <data name="NoEncryptionSessionRequiresHttps" xml:space="preserve"> + <value>Unless using transport layer encryption, "no-encryption" MUST NOT be used.</value> + </data> + <data name="NoSessionTypeFound" xml:space="preserve"> + <value>Diffie-Hellman session type '{0}' not found for OpenID {1}.</value> + </data> + <data name="OpenIdEndpointNotFound" xml:space="preserve"> + <value>No OpenID endpoint found.</value> + </data> + <data name="OperationOnlyValidForSetupRequiredState" xml:space="preserve"> + <value>This operation is only allowed when IAuthenticationResponse.State == AuthenticationStatus.SetupRequired.</value> + </data> + <data name="ProviderVersionUnrecognized" xml:space="preserve"> + <value>Unable to determine the version of the OpenID protocol implemented by the Provider at endpoint '{0}'.</value> + </data> + <data name="RealmCausedRedirectUponDiscovery" xml:space="preserve"> + <value>An HTTP request to the realm URL ({0}) resulted in a redirect, which is not allowed during relying party discovery.</value> + </data> + <data name="ReturnToNotUnderRealm" xml:space="preserve"> + <value>return_to '{0}' not under realm '{1}'.</value> + </data> + <data name="ReturnToParamDoesNotMatchRequestUrl" xml:space="preserve"> + <value>The {0} parameter ({1}) does not match the actual URL ({2}) the request was made with.</value> + </data> + <data name="ReturnToRequiredForResponse" xml:space="preserve"> + <value>The openid.return_to parameter is required in the request message in order to construct a response, but that parameter was missing.</value> + </data> + <data name="SignatureDoesNotIncludeMandatoryParts" xml:space="preserve"> + <value>The following parameter(s) are not included in the signature but must be: {0}</value> + </data> + <data name="SregInvalidBirthdate" xml:space="preserve"> + <value>Invalid birthdate value. Must be in the form yyyy-MM-dd.</value> + </data> + <data name="TypeMustImplementX" xml:space="preserve"> + <value>The type must implement {0}.</value> + </data> + <data name="UnsolicitedAssertionsNotAllowedFrom1xOPs" xml:space="preserve"> + <value>Unsolicited assertions are not allowed from 1.0 OpenID Providers.</value> + </data> + <data name="UserSetupUrlRequiredInImmediateNegativeResponse" xml:space="preserve"> + <value>The openid.user_setup_url parameter is required when sending negative assertion messages in response to immediate mode requests.</value> + </data> + <data name="XriResolutionFailed" xml:space="preserve"> + <value>XRI resolution failed.</value> + </data> + <data name="AttributeAlreadyAdded" xml:space="preserve"> + <value>An attribute with type URI '{0}' has already been added.</value> + </data> + <data name="AttributeTooManyValues" xml:space="preserve"> + <value>Only {0} values for attribute '{1}' were requested, but {2} were supplied.</value> + </data> + <data name="UnspecifiedDateTimeKindNotAllowed" xml:space="preserve"> + <value>Providing a DateTime whose Kind is Unspecified is not allowed.</value> + </data> + <data name="AssociationOrSessionTypeUnrecognizedOrNotSupported" xml:space="preserve"> + <value>The requested association type '{0}' with session type '{1}' is unrecognized or not supported by this Provider due to security requirements.</value> + </data> + <data name="IncompatibleAssociationAndSessionTypes" xml:space="preserve"> + <value>The Provider requested association type '{0}' and session type '{1}', which are not compatible with each other.</value> + </data> + <data name="CreateRequestAlreadyCalled" xml:space="preserve"> + <value>An authentication request has already been created using CreateRequest().</value> + </data> + <data name="OpenIdTextBoxEmpty" xml:space="preserve"> + <value>No OpenID url is provided.</value> + </data> + <data name="ClientScriptExtensionPropertyNameCollision" xml:space="preserve"> + <value>An extension with this property name ('{0}') has already been registered.</value> + </data> + <data name="ClientScriptExtensionTypeCollision" xml:space="preserve"> + <value>The extension '{0}' has already been registered.</value> + </data> + <data name="UnexpectedHttpStatusCode" xml:space="preserve"> + <value>Unexpected HTTP status code {0} {1} received in direct response.</value> + </data> + <data name="NotSupportedByAuthenticationSnapshot" xml:space="preserve"> + <value>This operation is not supported by serialized authentication responses. Try this operation from the LoggedIn event handler.</value> + </data> + <data name="NoRelyingPartyEndpointDiscovered" xml:space="preserve"> + <value>No XRDS document containing OpenID relying party endpoint information could be found at {0}.</value> + </data> + <data name="AbsoluteUriRequired" xml:space="preserve"> + <value>An absolute URI is required for this value.</value> + </data> + <data name="UnsolicitedAssertionForUnrelatedClaimedIdentifier" xml:space="preserve"> + <value>An unsolicited assertion cannot be sent for the claimed identifier {0} because this is not an authorized Provider for that identifier.</value> + </data> + <data name="MaximumAuthenticationTimeExpired" xml:space="preserve"> + <value>The maximum time allowed to complete authentication has been exceeded. Please try again.</value> + </data> + <data name="PrivateRPSecretNotFound" xml:space="preserve"> + <value>Unable to find the signing secret by the handle '{0}'.</value> + </data> + <data name="ResponseNotReady" xml:space="preserve"> + <value>The response is not ready. Use IsResponseReady to check whether a response is ready first.</value> + </data> + <data name="UnsupportedChannelConfiguration" xml:space="preserve"> + <value>This feature is unavailable due to an unrecognized channel configuration.</value> + </data> + <data name="IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent" xml:space="preserve"> + <value>The openid.identity and openid.claimed_id parameters must either be both present or both absent from the message.</value> + </data> + <data name="ReturnToRequiredForOperation" xml:space="preserve"> + <value>The ReturnTo property must not be null to support this operation.</value> + </data> + <data name="UnsolicitedAssertionRejectionRequiresNonceStore" xml:space="preserve"> + <value>Rejecting unsolicited assertions requires a nonce store and an association store.</value> + </data> + <data name="UnsolicitedAssertionsNotAllowed" xml:space="preserve"> + <value>Unsolicited assertions are not allowed at this relying party.</value> + </data> + <data name="DelegatingIdentifiersNotAllowed" xml:space="preserve"> + <value>Only OpenIDs issued directly by their OpenID Provider are allowed here.</value> + </data> + <data name="XriResolutionDisabled" xml:space="preserve"> + <value>XRI support has been disabled at this site.</value> + </data> + <data name="AssociationStoreRequired" xml:space="preserve"> + <value>No association store has been given but is required for the current configuration.</value> + </data> + <data name="UnexpectedEnumPropertyValue" xml:space="preserve"> + <value>The property {0} had unexpected value {1}.</value> + </data> + <data name="NoIdentifierSet" xml:space="preserve"> + <value>No identifier has been set.</value> + </data> + <data name="PropertyValueNotSupported" xml:space="preserve"> + <value>This property value is not supported by this control.</value> + </data> + <data name="ArgumentIsPpidIdentifier" xml:space="preserve"> + <value>This is already a PPID Identifier.</value> + </data> + <data name="RequireSslNotSatisfiedByAssertedClaimedId" xml:space="preserve"> + <value>Sorry. This site only accepts OpenIDs that are HTTPS-secured, but {0} is not a secure Identifier.</value> + </data> + <data name="BadExtension" xml:space="preserve"> + <value>The {0} extension failed to deserialize and will be skipped. {1}</value> + </data> + <data name="PositiveAssertionFromNonQualifiedProvider" xml:space="preserve"> + <value>An positive OpenID assertion was received from OP endpoint {0} and was rejected based on this site's security settings.</value> + </data> + <data name="HeadTagMustIncludeRunatServer" xml:space="preserve"> + <value>The HTML head tag must include runat="server".</value> + </data> + <data name="PropertyNotSet" xml:space="preserve"> + <value>The {0} property must be set first.</value> + </data> + <data name="X509CertificateNotTrusted" xml:space="preserve"> + <value>The X.509 certificate used to sign this document is not trusted.</value> + </data> + <data name="ClaimedIdentifierDefiesDotNetNormalization" xml:space="preserve"> + <value>This OpenID exploits features that this relying party cannot reliably verify. Please try logging in with a human-readable OpenID or from a different OpenID Provider.</value> + </data> + <data name="MissingElement" xml:space="preserve"> + <value>Missing {0} element.</value> + </data> + <data name="DiffieHellmanAssociationRequired" xml:space="preserve"> + <value>The associate request instance must be a Diffie-Hellman instance.</value> + </data> + <data name="InvalidDSig" xml:space="preserve"> + <value>Invalid XmlDSig signature on XRDS document.</value> + </data> + <data name="MisdirectedSigningCertificate" xml:space="preserve"> + <value>X.509 signing certificate issued to {0}, but a certificate for {1} was expected.</value> + </data> + <data name="PopupRedirectMissingResponse" xml:space="preserve"> + <value>OpenID popup window or iframe did not recognize an OpenID response in the request.</value> + </data> + <data name="UnsupportedCanonicalizationMethod" xml:space="preserve"> + <value>Unrecognized or missing canonicalization method.</value> + </data> + <data name="UnsupportedSignatureMethod" xml:space="preserve"> + <value>Unrecognized or missing signature method.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.sr.resx b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.sr.resx new file mode 100644 index 0000000..0df62c0 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdStrings.sr.resx @@ -0,0 +1,340 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AssociationSecretAndTypeLengthMismatch" xml:space="preserve"> + <value>Dužina deljene tajne ({0}) ne slaže se sa dužinom zahtevanom od povezujućeg tipa ('{1}').</value> + </data> + <data name="AssociationSecretHashLengthMismatch" xml:space="preserve"> + <value>Dužina šifrovane deljene tajne ({0}) ne slaže se sa dužinom hashing algoritma ({1}).</value> + </data> + <data name="AssociationStoreRequiresNonceStore" xml:space="preserve"> + <value>Ako je dato povezujuće skladište, jedinstveni identifikator skladišta takođe mora biti prisutan.</value> + </data> + <data name="BadAssociationPrivateData" xml:space="preserve"> + <value>Dostavljeni privatni podaci ne slažu se ni sa jednim poznatim Association tipom. Dužina im je možda prekratka ili su podaci neispravni.</value> + </data> + <data name="CallbackArgumentsRequireSecretStore" xml:space="preserve"> + <value>Callback argumenti su podržani jedino kada je {0} dustupan za {1}.</value> + </data> + <data name="CallDeserializeBeforeCreateResponse" xml:space="preserve"> + <value>Simple Registration zahtev može generisati odgovor jedino na prijemnoj strani.</value> + </data> + <data name="ClaimedIdAndLocalIdMustBothPresentOrAbsent" xml:space="preserve"> + <value>openid.claimed_id i openid.identity parametri moraju istovremeno biti prisutni ili nedostajati.</value> + </data> + <data name="ClaimedIdentifierCannotBeSetOnDelegatedAuthentication" xml:space="preserve"> + <value>ClaimedIdentifier svojstvo ne može se podesiti kada IsDelegatedIdentifier ima vrednost true da bi se izbeglo narušavanje OpenID URL delegiranja.</value> + </data> + <data name="ClaimedIdentifierMustBeSetFirst" xml:space="preserve"> + <value>ClaimedIdentifier svojstvo se najpre mora podesiti.</value> + </data> + <data name="DiffieHellmanRequiredPropertiesNotSet" xml:space="preserve"> + <value>Sledeća svojstva moraju biti podešena pre nego što Diffie-Hellmanov algoritam može generisati javni ključ: {0}</value> + </data> + <data name="ExplicitHttpUriSuppliedWithSslRequirement" xml:space="preserve"> + <value>URI nije SSL a svojstvo requireSslDiscovery je podešeno na true.</value> + </data> + <data name="ExtensionAlreadyAddedWithSameTypeURI" xml:space="preserve"> + <value>Prostor deljenja proširenja '{0}' je već dodat. Samo jedno proširenje po prostoru je dozvoljeno u datom zahtevu.</value> + </data> + <data name="ExtensionLookupSupportUnavailable" xml:space="preserve"> + <value>Ne može se tražiti podrška za proširenja na rehidriranom ServiceEndpoint.</value> + </data> + <data name="FragmentNotAllowedOnXRIs" xml:space="preserve"> + <value>Segmenti fragmenta se ne primenjuju na XRI identifikatore.</value> + </data> + <data name="IdentifierSelectRequiresMatchingIdentifiers" xml:space="preserve"> + <value>ClaimedIdentifier i LocalIdentifier moraju biti isti kada IsIdentifierSelect ima vrednost true.</value> + </data> + <data name="IndirectErrorFormattedMessage" xml:space="preserve"> + <value>{0} (Kontakt: {1}, Referenca: {2})</value> + </data> + <data name="InvalidCharacterInKeyValueFormInput" xml:space="preserve"> + <value>Ne može se enkodovati '{0}' jer sadrži nevalidan znak za Key-Value Form enkodiranje. (linija {1}: '{2}')</value> + </data> + <data name="InvalidKeyValueFormCharacterMissing" xml:space="preserve"> + <value>Ne može se dekodovati Key-Value Form jer je pronađena linija bez '{0}' znaka. (linija {1}: '{2}')</value> + </data> + <data name="InvalidScheme" xml:space="preserve"> + <value>Šema mora biti http ili https a bila je '{0}'.</value> + </data> + <data name="InvalidUri" xml:space="preserve"> + <value>Vrednost '{0}' nije validan URI.</value> + </data> + <data name="InvalidXri" xml:space="preserve"> + <value>Nije prepoznat XRI format: '{0}'.</value> + </data> + <data name="IssuedAssertionFailsIdentifierDiscovery" xml:space="preserve"> + <value>OpenID Provider dao je iskaz za Identifier čije se informacije o pronalaženju ne slažu. +Informacije o krajnoj tački iskaza: +{0} +Informacije o otkrivenoj krajnjoj tački: +{1}</value> + </data> + <data name="KeysListAndDictionaryDoNotMatch" xml:space="preserve"> + <value>Lista ključeva se ne slaže sa ponuđenim rečnikom.</value> + </data> + <data name="MatchingArgumentsExpected" xml:space="preserve"> + <value>Parametri '{0}' i '{1}' moraju oba biti ili ne smeju oba biti '{2}'.</value> + </data> + <data name="NoAssociationTypeFoundByLength" xml:space="preserve"> + <value>Ni jedan prepoznati tip asociranja se ne uklapa sa zahtevanom dužinom {0}.</value> + </data> + <data name="NoAssociationTypeFoundByName" xml:space="preserve"> + <value>Ni jedan prepoznati tip asociranja se ne uklapa sa zahtevanim imenom '{0}'.</value> + </data> + <data name="NoEncryptionSessionRequiresHttps" xml:space="preserve"> + <value>Osim ako se ne koristi enkripcija transportnog sloja, "no-encryption" NE SME biti korišćen.</value> + </data> + <data name="NoSessionTypeFound" xml:space="preserve"> + <value>Diffie-Hellman sesija tipa '{0}' nije pronađena za OpenID {1}.</value> + </data> + <data name="OpenIdEndpointNotFound" xml:space="preserve"> + <value>Nijedna OpenID krajnja tačka nije pronađena.</value> + </data> + <data name="OperationOnlyValidForSetupRequiredState" xml:space="preserve"> + <value>Ova operacija je jedino dozvoljena kada je IAuthenticationResponse.State == AuthenticationStatus.SetupRequired.</value> + </data> + <data name="ProviderVersionUnrecognized" xml:space="preserve"> + <value>Nije moguće utvrditi verziju OpenID protokola implementiranog od strane Provider-a na krajnjoj tački '{0}'.</value> + </data> + <data name="RealmCausedRedirectUponDiscovery" xml:space="preserve"> + <value>HTTP zahtev ka URL-u domena ({0}) rezultovao je redirekcijom, koja nije dozvoljena u togu pronalaženja Relying Party.</value> + </data> + <data name="ReturnToNotUnderRealm" xml:space="preserve"> + <value>return_to '{0}' nije unutar domena '{1}'.</value> + </data> + <data name="ReturnToParamDoesNotMatchRequestUrl" xml:space="preserve"> + <value>{0} parametar ({1}) se ne slaže sa trenutnim URL ({2}) sa kojim je zahtev napravljen.</value> + </data> + <data name="ReturnToRequiredForResponse" xml:space="preserve"> + <value>openid.return_to parametar je neophodan u poruci zahteva da bi se konstruisao odgovor, ali ovaj parametar nedostaje.</value> + </data> + <data name="SignatureDoesNotIncludeMandatoryParts" xml:space="preserve"> + <value>Sledeći parametri nisu uključeni u potpis a moraju da budu: {0}</value> + </data> + <data name="SregInvalidBirthdate" xml:space="preserve"> + <value>Neispravna vrednost za datum rođenja. Mora biti u formi gggg-MM-dd.</value> + </data> + <data name="TypeMustImplementX" xml:space="preserve"> + <value>Tip mora implementirati {0}.</value> + </data> + <data name="UnsolicitedAssertionsNotAllowedFrom1xOPs" xml:space="preserve"> + <value>Nezahtevani iskazi nisu dozvoljeni od strane 1.0 OpenID Providers.</value> + </data> + <data name="UserSetupUrlRequiredInImmediateNegativeResponse" xml:space="preserve"> + <value>openid.user_setup_url parametar je neophodan prilikom slanja negativnih poruka sa iskazima prilikom odgovaranja na zahteve u trenutnom modu.</value> + </data> + <data name="XriResolutionFailed" xml:space="preserve"> + <value>XRI razrešivanje neuspešno.</value> + </data> + <data name="StoreRequiredWhenNoHttpContextAvailable" xml:space="preserve"> + <value>Tekući HttpContext nije detektovan, tako da {0} instanca mora biti eksplicitno postavljena ili specificirana u .config fajlu. Pozvati preklopljeni konstruktor koji uzima parametar {0}.</value> + </data> + <data name="AttributeAlreadyAdded" xml:space="preserve"> + <value>Atribut sa URI tipom '{0}' je već dodat.</value> + </data> + <data name="AttributeTooManyValues" xml:space="preserve"> + <value>Samo {0} vrednosti za atribut '{1}' su zahtevane, ali {2} su ponuđene.</value> + </data> + <data name="UnspecifiedDateTimeKindNotAllowed" xml:space="preserve"> + <value>Prosleđivanje objekta tipa DateTime čije svojstvo Kind ima vrednost Unspecified nije dozvoljeno.</value> + </data> + <data name="AssociationOrSessionTypeUnrecognizedOrNotSupported" xml:space="preserve"> + <value>Zahtevani tip asocijacije '{0}' sa sesijom tipa '{1}' nije prepoznat ili nije podržan od strane ovog Provider-a zbog bezbedonosnih zahteva.</value> + </data> + <data name="IncompatibleAssociationAndSessionTypes" xml:space="preserve"> + <value>Provider je zahtevao asocijaciju tipa '{0}' i sesiju tipa '{1}', koje nisu međusobno kompatibilne.</value> + </data> + <data name="CreateRequestAlreadyCalled" xml:space="preserve"> + <value>Zahtev za autentifikacijom je već kreiran korišćenjem CreateRequest().</value> + </data> + <data name="OpenIdTextBoxEmpty" xml:space="preserve"> + <value>Nijedan OpenID url nije ponuđen.</value> + </data> + <data name="ClientScriptExtensionPropertyNameCollision" xml:space="preserve"> + <value>Ekstenzija sa svojstvom ovog imena ('{0}') je već registrovana.</value> + </data> + <data name="ClientScriptExtensionTypeCollision" xml:space="preserve"> + <value>Ekstenzija '{0}' je već registrovana.</value> + </data> + <data name="UnexpectedHttpStatusCode" xml:space="preserve"> + <value>Neočekivani HTTP statusni kod {0} {1} primljen u direktnom odgovoru.</value> + </data> + <data name="NotSupportedByAuthenticationSnapshot" xml:space="preserve"> + <value>Ova operacija nije podržana od strane serijalizovanih odgovora za autentifikaciju. Pokušati ovu operaciju iz LoggedIn handler-a događaja.</value> + </data> + <data name="NoRelyingPartyEndpointDiscovered" xml:space="preserve"> + <value>Nijedan XRDS dokument koji sadrži informaciju o OpenID Relying Party krajnjoj tački nije pronadjen u {0}.</value> + </data> + <data name="AbsoluteUriRequired" xml:space="preserve"> + <value>Absolutni URI je zahtevan za ovu vrednost.</value> + </data> + <data name="UnsolicitedAssertionForUnrelatedClaimedIdentifier" xml:space="preserve"> + <value>Nezahtevani iskaz ne može biti poslat za navedeni identifikator {0} jer ovo nije autorizovani Provider za taj identifikator.</value> + </data> + <data name="MaximumAuthenticationTimeExpired" xml:space="preserve"> + <value>Maksimalno dozvoljeno vreme za kompletiranje autentifikacije je isteklo. Molimo pokušajte ponovo.</value> + </data> + <data name="PrivateRPSecretNotFound" xml:space="preserve"> + <value>Ne može se pronaći tajna za potpisivanje od strane handle-a '{0}'.</value> + </data> + <data name="ResponseNotReady" xml:space="preserve"> + <value>Odgovor nije spreman. Koristiti najpre IsResponseReady za proveru da li je odgovor spreman.</value> + </data> + <data name="UnsupportedChannelConfiguration" xml:space="preserve"> + <value>Ovo svojstvo nije dostupno zbog nepoznate konfiguracije kanala.</value> + </data> + <data name="IdentityAndClaimedIdentifierMustBeBothPresentOrAbsent" xml:space="preserve"> + <value>openid.identity i openid.claimed_id parametri moraju istovremeno biti prisutna ili istovremeno odsutna u poruci.</value> + </data> + <data name="ReturnToRequiredForOperation" xml:space="preserve"> + <value>Svojstvo ReturnTo ne sme biti null da bi se podržala ova operacija.</value> + </data> + <data name="UnsolicitedAssertionRejectionRequiresNonceStore" xml:space="preserve"> + <value>Odbijanje nezahtevanih iskaza zahteva skladište jedinstvenih identifikatora i skladište asocijacija.</value> + </data> + <data name="UnsolicitedAssertionsNotAllowed" xml:space="preserve"> + <value>Nezahtevani iskazi nisu dozvoljeni od strane ovog Relying Party.</value> + </data> + <data name="DelegatingIdentifiersNotAllowed" xml:space="preserve"> + <value>Samo OpenID-jevi izdati direktno od strane njihovog OpenID Provider-a su ovde dozvoljeni.</value> + </data> + <data name="XriResolutionDisabled" xml:space="preserve"> + <value>XRI podrška je onemogućena na ovom sajtu.</value> + </data> + <data name="AssociationStoreRequired" xml:space="preserve"> + <value>Skladište asocijacija nije dato a zahtevano je za trenutnu konfiguraciju.</value> + </data> + <data name="UnexpectedEnumPropertyValue" xml:space="preserve"> + <value>Svojstvo {0} je imalo neočekivanu vrednost {1}.</value> + </data> + <data name="NoIdentifierSet" xml:space="preserve"> + <value>Ni jedan identifikator nije podešen.</value> + </data> + <data name="PropertyValueNotSupported" xml:space="preserve"> + <value>Ova vrednost svojstva nije podržana od strane ove kontrole.</value> + </data> + <data name="ArgumentIsPpidIdentifier" xml:space="preserve"> + <value>Ovo je već PPID Identifier.</value> + </data> + <data name="RequireSslNotSatisfiedByAssertedClaimedId" xml:space="preserve"> + <value>Žao nam je. Ovaj sajt jedino prihvata OpenID-jeve koji su HTTPS-bezbedni, a {0} nije bezbedni Identifier.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/OpenIdUtilities.cs b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdUtilities.cs new file mode 100644 index 0000000..68babd9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdUtilities.cs @@ -0,0 +1,195 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdUtilities.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.Text.RegularExpressions; + using System.Web.UI; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A set of utilities especially useful to OpenID. + /// </summary> + public static class OpenIdUtilities { + /// <summary> + /// The prefix to designate this library's proprietary parameters added to the protocol. + /// </summary> + internal const string CustomParameterPrefix = "dnoa."; + + /// <summary> + /// Creates a random association handle. + /// </summary> + /// <returns>The association handle.</returns> + public static string GenerateRandomAssociationHandle() { + Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>())); + + // Generate the handle. It must be unique, and preferably unpredictable, + // so we use a time element and a random data element to generate it. + string uniq = MessagingUtilities.GetCryptoRandomDataAsBase64(4); + return string.Format(CultureInfo.InvariantCulture, "{{{0}}}{{{1}}}", DateTime.UtcNow.Ticks, uniq); + } + + /// <summary> + /// Gets the OpenID protocol instance for the version in a message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>The OpenID protocol instance.</returns> + internal static Protocol GetProtocol(this IProtocolMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + return Protocol.Lookup(message.Version); + } + + /// <summary> + /// Changes the position of some element in a list. + /// </summary> + /// <typeparam name="T">The type of elements stored in the list.</typeparam> + /// <param name="list">The list to be modified.</param> + /// <param name="position">The new position for the given element.</param> + /// <param name="value">The element to move within the list.</param> + /// <exception cref="InternalErrorException">Thrown if the element does not already exist in the list.</exception> + internal static void MoveTo<T>(this IList<T> list, int position, T value) { + ErrorUtilities.VerifyInternal(list.Remove(value), "Unable to find element in list."); + list.Insert(position, value); + } + + /// <summary> + /// Corrects any URI decoding the Provider may have inappropriately done + /// to our return_to URL, resulting in an otherwise corrupted base64 encoded value. + /// </summary> + /// <param name="value">The base64 encoded value. May be null.</param> + /// <returns> + /// The value; corrected if corruption had occurred. + /// </returns> + /// <remarks> + /// AOL may have incorrectly URI-decoded the token for us in the return_to, + /// resulting in a token URI-decoded twice by the time we see it, and no + /// longer being a valid base64 string. + /// It turns out that the only symbols from base64 that is also encoded + /// in URI encoding rules are the + and / characters. + /// AOL decodes the %2b sequence to the + character + /// and the %2f sequence to the / character (it shouldn't decode at all). + /// When we do our own URI decoding, the + character becomes a space (corrupting base64) + /// but the / character remains a /, so no further corruption happens to this character. + /// So to correct this we just need to change any spaces we find in the token + /// back to + characters. + /// </remarks> + internal static string FixDoublyUriDecodedBase64String(string value) { + if (value == null) { + return null; + } + + if (value.Contains(" ")) { + Logger.OpenId.Error("Deserializing a corrupted token. The OpenID Provider may have inappropriately decoded the return_to URL before sending it back to us."); + value = value.Replace(' ', '+'); // Undo any extra decoding the Provider did + } + + return value; + } + + /// <summary> + /// Rounds the given <see cref="DateTime"/> downward to the whole second. + /// </summary> + /// <param name="dateTime">The DateTime object to adjust.</param> + /// <returns>The new <see cref="DateTime"/> value.</returns> + internal static DateTime CutToSecond(DateTime dateTime) { + return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); + } + + /// <summary> + /// Gets the fully qualified Realm URL, given a Realm that may be relative to a particular page. + /// </summary> + /// <param name="page">The hosting page that has the realm value to resolve.</param> + /// <param name="realm">The realm, which may begin with "*." or "~/".</param> + /// <param name="requestContext">The request context.</param> + /// <returns>The fully-qualified realm.</returns> + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")] + internal static UriBuilder GetResolvedRealm(Page page, string realm, HttpRequestInfo requestContext) { + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(requestContext != null); + + // Allow for *. realm notation, as well as ASP.NET ~/ shortcuts. + + // We have to temporarily remove the *. notation if it's there so that + // the rest of our URL manipulation will succeed. + bool foundWildcard = false; + + // Note: we don't just use string.Replace because poorly written URLs + // could potentially have multiple :// sequences in them. + MatchEvaluator matchDelegate = delegate(Match m) { + foundWildcard = true; + return m.Groups[1].Value; + }; + string realmNoWildcard = Regex.Replace(realm, @"^(\w+://)\*\.", matchDelegate); + + UriBuilder fullyQualifiedRealm = new UriBuilder( + new Uri(requestContext.UrlBeforeRewriting, page.ResolveUrl(realmNoWildcard))); + + if (foundWildcard) { + fullyQualifiedRealm.Host = "*." + fullyQualifiedRealm.Host; + } + + // Is it valid? + new Realm(fullyQualifiedRealm); // throws if not valid + + return fullyQualifiedRealm; + } + + /// <summary> + /// Gets the extension factories from the extension aggregator on an OpenID channel. + /// </summary> + /// <param name="channel">The channel.</param> + /// <returns>The list of factories that will be used to generate extension instances.</returns> + /// <remarks> + /// This is an extension method on <see cref="Channel"/> rather than an instance + /// method on <see cref="OpenIdChannel"/> because the <see cref="OpenIdRelyingParty"/> + /// and <see cref="OpenIdProvider"/> classes don't strong-type to <see cref="OpenIdChannel"/> + /// to allow flexibility in the specific type of channel the user (or tests) + /// can plug in. + /// </remarks> + internal static IList<IOpenIdExtensionFactory> GetExtensionFactories(this Channel channel) { + Contract.Requires<ArgumentNullException>(channel != null); + + var extensionsBindingElement = channel.BindingElements.OfType<ExtensionsBindingElement>().SingleOrDefault(); + ErrorUtilities.VerifyOperation(extensionsBindingElement != null, OpenIdStrings.UnsupportedChannelConfiguration); + IOpenIdExtensionFactory factory = extensionsBindingElement.ExtensionFactory; + var aggregator = factory as OpenIdExtensionFactoryAggregator; + ErrorUtilities.VerifyOperation(aggregator != null, OpenIdStrings.UnsupportedChannelConfiguration); + return aggregator.Factories; + } + + /// <summary> + /// Determines whether the association with the specified handle is (still) valid. + /// </summary> + /// <param name="associationStore">The association store.</param> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// <c>true</c> if the specified containing message is valid; otherwise, <c>false</c>. + /// </returns> + internal static bool IsValid(this IProviderAssociationStore associationStore, IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) { + Contract.Requires<ArgumentNullException>(associationStore != null); + Contract.Requires<ArgumentNullException>(containingMessage != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle)); + try { + return associationStore.Deserialize(containingMessage, isPrivateAssociation, handle) != null; + } catch (ProtocolException) { + return false; + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/OpenIdXrdsHelper.cs b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdXrdsHelper.cs new file mode 100644 index 0000000..00468ed --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/OpenIdXrdsHelper.cs @@ -0,0 +1,212 @@ +//----------------------------------------------------------------------- +// <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 OpenIdXrdsHelper { + /// <summary> + /// Finds the Relying Party return_to receiving endpoints. + /// </summary> + /// <param name="xrds">The XrdsDocument instance to use in this process.</param> + /// <returns>A sequence of Relying Party descriptors for the return_to endpoints.</returns> + /// <remarks> + /// This is useful for Providers to send unsolicited assertions to Relying Parties, + /// or for Provider's to perform RP discovery/verification as part of authentication. + /// </remarks> + internal static IEnumerable<RelyingPartyEndpointDescription> FindRelyingPartyReceivingEndpoints(this XrdsDocument xrds) { + Contract.Requires<ArgumentNullException>(xrds != null); + Contract.Ensures(Contract.Result<IEnumerable<RelyingPartyEndpointDescription>>() != null); + + return from service in xrds.FindReturnToServices() + from uri in service.UriElements + select new RelyingPartyEndpointDescription(uri.Uri, service.TypeElementUris); + } + + /// <summary> + /// Finds the icons the relying party wants an OP to display as part of authentication, + /// per the UI extension spec. + /// </summary> + /// <param name="xrds">The XrdsDocument to search.</param> + /// <returns>A sequence of the icon URLs in preferred order.</returns> + internal static IEnumerable<Uri> FindRelyingPartyIcons(this XrdsDocument xrds) { + Contract.Requires<ArgumentNullException>(xrds != null); + Contract.Ensures(Contract.Result<IEnumerable<Uri>>() != null); + + return from xrd in xrds.XrdElements + from service in xrd.OpenIdRelyingPartyIcons + from uri in service.UriElements + select uri.Uri; + } + + /// <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; + } + + /// <summary> + /// Enumerates the XRDS service elements that describe OpenID Relying Party return_to URLs + /// that can receive authentication 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> FindReturnToServices(this XrdsDocument xrds) { + Contract.Requires<ArgumentNullException>(xrds != null); + Contract.Ensures(Contract.Result<IEnumerable<ServiceElement>>() != null); + + return from xrd in xrds.XrdElements + from service in xrd.OpenIdRelyingPartyReturnToServices + select service; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Protocol.cs b/src/DotNetOpenAuth.OpenId/OpenId/Protocol.cs new file mode 100644 index 0000000..5aacfd2 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Protocol.cs @@ -0,0 +1,472 @@ +// <auto-generated/> // disable StyleCop on this file +//----------------------------------------------------------------------- +// <copyright file="Protocol.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using System.Globalization; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Diagnostics; + + /// <summary> + /// An enumeration of the OpenID protocol versions supported by this library. + /// </summary> + public enum ProtocolVersion { + /// <summary> + /// OpenID Authentication 1.0 + /// </summary> + V10, + /// <summary> + /// OpenID Authentication 1.1 + /// </summary> + V11, + /// <summary> + /// OpenID Authentication 2.0 + /// </summary> + V20, + } + + /// <summary> + /// Tracks the several versions of OpenID this library supports and the unique + /// constants to each version used in the protocol. + /// </summary> + [DebuggerDisplay("OpenID {Version}")] + internal sealed class Protocol { + /// <summary> + /// The value of the openid.ns parameter in the OpenID 2.0 specification. + /// </summary> + internal const string OpenId2Namespace = "http://specs.openid.net/auth/2.0"; + + /// <summary> + /// Scans a list for matches with some element of the OpenID protocol, + /// searching from newest to oldest protocol for the first and best match. + /// </summary> + /// <typeparam name="T">The type of element retrieved from the <see cref="Protocol"/> instance.</typeparam> + /// <param name="elementOf">Takes a <see cref="Protocol"/> instance and returns an element of it.</param> + /// <param name="list">The list to scan for matches.</param> + /// <returns>The protocol with the element that matches some item in the list.</returns> + internal static Protocol FindBestVersion<T>(Func<Protocol, T> elementOf, IEnumerable<T> list) { + foreach (var protocol in Protocol.AllVersions) { + foreach (var item in list) { + if (item != null && item.Equals(elementOf(protocol))) + return protocol; + } + } + return null; + } + + Protocol(QueryParameters queryBits) { + openidnp = queryBits; + openid = new QueryParameters(queryBits); + } + + // Well-known, supported versions of the OpenID spec. + public static readonly Protocol V10 = new Protocol(new QueryParameters()) { + Version = new Version(1, 0), + XmlNamespace = "http://openid.net/xmlns/1.0", + QueryDeclaredNamespaceVersion = null, + ClaimedIdentifierServiceTypeURI = "http://openid.net/signon/1.0", + OPIdentifierServiceTypeURI = null, // not supported + ClaimedIdentifierForOPIdentifier = null, // not supported + RPReturnToTypeURI = null, // not supported + HtmlDiscoveryProviderKey = "openid.server", + HtmlDiscoveryLocalIdKey = "openid.delegate", + }; + public static readonly Protocol V11 = new Protocol(new QueryParameters()) { + Version = new Version(1, 1), + XmlNamespace = "http://openid.net/xmlns/1.0", + QueryDeclaredNamespaceVersion = null, + ClaimedIdentifierServiceTypeURI = "http://openid.net/signon/1.1", + OPIdentifierServiceTypeURI = null, // not supported + ClaimedIdentifierForOPIdentifier = null, // not supported + RPReturnToTypeURI = null, // not supported + HtmlDiscoveryProviderKey = "openid.server", + HtmlDiscoveryLocalIdKey = "openid.delegate", + }; + public static readonly Protocol V20 = new Protocol(new QueryParameters() { + Realm = "realm", + op_endpoint = "op_endpoint", + response_nonce = "response_nonce", + error_code = "error_code", + user_setup_url = null, + }) { + Version = new Version(2, 0), + XmlNamespace = null, // no longer applicable + QueryDeclaredNamespaceVersion = Protocol.OpenId2Namespace, + ClaimedIdentifierServiceTypeURI = "http://specs.openid.net/auth/2.0/signon", + OPIdentifierServiceTypeURI = "http://specs.openid.net/auth/2.0/server", + ClaimedIdentifierForOPIdentifier = "http://specs.openid.net/auth/2.0/identifier_select", + RPReturnToTypeURI = "http://specs.openid.net/auth/2.0/return_to", + HtmlDiscoveryProviderKey = "openid2.provider", + HtmlDiscoveryLocalIdKey = "openid2.local_id", + Args = new QueryArguments() { + SessionType = new QueryArguments.SessionTypes() { + NoEncryption = "no-encryption", + DH_SHA256 = "DH-SHA256", + DH_SHA384 = "DH-SHA384", + DH_SHA512 = "DH-SHA512", + }, + SignatureAlgorithm = new QueryArguments.SignatureAlgorithms() { + HMAC_SHA256 = "HMAC-SHA256", + HMAC_SHA384 = "HMAC-SHA384", + HMAC_SHA512 = "HMAC-SHA512", + }, + Mode = new QueryArguments.Modes() { + setup_needed = "setup_needed", + }, + }, + }; + + /// <summary> + /// A list of all supported OpenID versions, in order starting from newest version. + /// </summary> + public readonly static List<Protocol> AllVersions = new List<Protocol>() { V20, V11, V10 }; + + /// <summary> + /// A list of all supported OpenID versions, in order starting from newest version. + /// V1.1 and V1.0 are considered the same and only V1.1 is in the list. + /// </summary> + public readonly static List<Protocol> AllPracticalVersions = new List<Protocol>() { V20, V11 }; + + /// <summary> + /// The default (or most recent) supported version of the OpenID protocol. + /// </summary> + public readonly static Protocol Default = AllVersions[0]; + public static Protocol Lookup(Version version) { + foreach (Protocol protocol in AllVersions) { + if (protocol.Version == version) return protocol; + } + throw new ArgumentOutOfRangeException("version"); + } + public static Protocol Lookup(ProtocolVersion version) { + switch (version) { + case ProtocolVersion.V10: return Protocol.V10; + case ProtocolVersion.V11: return Protocol.V11; + case ProtocolVersion.V20: return Protocol.V20; + default: throw new ArgumentOutOfRangeException("version"); + } + } + /// <summary> + /// Attempts to detect the right OpenID protocol version based on the contents + /// of an incoming OpenID <i>indirect</i> message or <i>direct request</i>. + /// </summary> + internal static Protocol Detect(IDictionary<string, string> query) { + Contract.Requires<ArgumentNullException>(query != null); + return query.ContainsKey(V20.openid.ns) ? V20 : V11; + } + /// <summary> + /// Attempts to detect the right OpenID protocol version based on the contents + /// of an incoming OpenID <i>direct</i> response message. + /// </summary> + internal static Protocol DetectFromDirectResponse(IDictionary<string, string> query) { + Contract.Requires<ArgumentNullException>(query != null); + return query.ContainsKey(V20.openidnp.ns) ? V20 : V11; + } + /// <summary> + /// Attemps to detect the highest OpenID protocol version supported given a set + /// of XRDS Service Type URIs included for some service. + /// </summary> + internal static Protocol Detect(IEnumerable<string> serviceTypeURIs) { + Contract.Requires<ArgumentNullException>(serviceTypeURIs != null); + return FindBestVersion(p => p.OPIdentifierServiceTypeURI, serviceTypeURIs) ?? + FindBestVersion(p => p.ClaimedIdentifierServiceTypeURI, serviceTypeURIs) ?? + FindBestVersion(p => p.RPReturnToTypeURI, serviceTypeURIs); + } + + /// <summary> + /// The OpenID version that this <see cref="Protocol"/> instance describes. + /// </summary> + public Version Version; + /// <summary> + /// Returns the <see cref="ProtocolVersion"/> enum value for the <see cref="Protocol"/> instance. + /// </summary> + public ProtocolVersion ProtocolVersion { + get { + switch (Version.Major) { + case 1: return ProtocolVersion.V11; + case 2: return ProtocolVersion.V20; + default: throw new ArgumentException(null); // this should never happen + } + } + } + /// <summary> + /// The namespace of OpenId 1.x elements in XRDS documents. + /// </summary> + public string XmlNamespace; + /// <summary> + /// The value of the openid.ns parameter that appears on the query string + /// whenever data is passed between relying party and provider for OpenID 2.0 + /// and later. + /// </summary> + public string QueryDeclaredNamespaceVersion; + /// <summary> + /// The XRD/Service/Type value discovered in an XRDS document when + /// "discovering" on a Claimed Identifier (http://andrewarnott.yahoo.com) + /// </summary> + public string ClaimedIdentifierServiceTypeURI; + /// <summary> + /// The XRD/Service/Type value discovered in an XRDS document when + /// "discovering" on an OP Identifier rather than a Claimed Identifier. + /// (http://yahoo.com) + /// </summary> + public string OPIdentifierServiceTypeURI; + /// <summary> + /// The XRD/Service/Type value discovered in an XRDS document when + /// "discovering" on a Realm URL and looking for the endpoint URL + /// that can receive authentication assertions. + /// </summary> + public string RPReturnToTypeURI; + /// <summary> + /// Used as the Claimed Identifier and the OP Local Identifier when + /// the User Supplied Identifier is an OP Identifier. + /// </summary> + public string ClaimedIdentifierForOPIdentifier; + /// <summary> + /// The value of the 'rel' attribute in an HTML document's LINK tag + /// when the same LINK tag's HREF attribute value contains the URL to an + /// OP Endpoint URL. + /// </summary> + public string HtmlDiscoveryProviderKey; + /// <summary> + /// The value of the 'rel' attribute in an HTML document's LINK tag + /// when the same LINK tag's HREF attribute value contains the URL to use + /// as the OP Local Identifier. + /// </summary> + public string HtmlDiscoveryLocalIdKey; + /// <summary> + /// Parts of the protocol that define parameter names that appear in the + /// query string. Each parameter name is prefixed with 'openid.'. + /// </summary> + public readonly QueryParameters openid; + /// <summary> + /// Parts of the protocol that define parameter names that appear in the + /// query string. Each parameter name is NOT prefixed with 'openid.'. + /// </summary> + public readonly QueryParameters openidnp; + /// <summary> + /// The various 'constants' that appear as parameter arguments (values). + /// </summary> + public QueryArguments Args = new QueryArguments(); + + internal sealed class QueryParameters { + /// <summary> + /// The value "openid." + /// </summary> + public readonly string Prefix = "openid."; + [SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily")] + public QueryParameters() { } + [SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily")] + public QueryParameters(QueryParameters addPrefixTo) { + ns = addPrefix(addPrefixTo.ns); + return_to = addPrefix(addPrefixTo.return_to); + Realm = addPrefix(addPrefixTo.Realm); + mode = addPrefix(addPrefixTo.mode); + error = addPrefix(addPrefixTo.error); + error_code = addPrefix(addPrefixTo.error_code); + identity = addPrefix(addPrefixTo.identity); + op_endpoint = addPrefix(addPrefixTo.op_endpoint); + response_nonce = addPrefix(addPrefixTo.response_nonce); + claimed_id = addPrefix(addPrefixTo.claimed_id); + expires_in = addPrefix(addPrefixTo.expires_in); + assoc_type = addPrefix(addPrefixTo.assoc_type); + assoc_handle = addPrefix(addPrefixTo.assoc_handle); + session_type = addPrefix(addPrefixTo.session_type); + is_valid = addPrefix(addPrefixTo.is_valid); + sig = addPrefix(addPrefixTo.sig); + signed = addPrefix(addPrefixTo.signed); + user_setup_url = addPrefix(addPrefixTo.user_setup_url); + invalidate_handle = addPrefix(addPrefixTo.invalidate_handle); + dh_modulus = addPrefix(addPrefixTo.dh_modulus); + dh_gen = addPrefix(addPrefixTo.dh_gen); + dh_consumer_public = addPrefix(addPrefixTo.dh_consumer_public); + dh_server_public = addPrefix(addPrefixTo.dh_server_public); + enc_mac_key = addPrefix(addPrefixTo.enc_mac_key); + mac_key = addPrefix(addPrefixTo.mac_key); + } + string addPrefix(string original) { + return (original != null) ? Prefix + original : null; + } + // These fields default to 1.x specifications, and are overridden + // as necessary by later versions in the Protocol class initializers. + // Null values in any version suggests that that feature is absent from that version. + public string ns = "ns"; + public string return_to = "return_to"; + public string Realm = "trust_root"; + public string mode = "mode"; + public string error = "error"; + public string error_code = null; + public string identity = "identity"; + public string op_endpoint = null; + public string response_nonce = null; + public string claimed_id = "claimed_id"; + public string expires_in = "expires_in"; + public string assoc_type = "assoc_type"; + public string assoc_handle = "assoc_handle"; + public string session_type = "session_type"; + public string is_valid = "is_valid"; + public string sig = "sig"; + public string signed = "signed"; + public string user_setup_url = "user_setup_url"; + public string invalidate_handle = "invalidate_handle"; + public string dh_modulus = "dh_modulus"; + public string dh_gen = "dh_gen"; + public string dh_consumer_public = "dh_consumer_public"; + public string dh_server_public = "dh_server_public"; + public string enc_mac_key = "enc_mac_key"; + public string mac_key = "mac_key"; + +#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(!string.IsNullOrEmpty(this.Prefix)); + } +#endif + } + + internal sealed class QueryArguments { + public ErrorCodes ErrorCode = new ErrorCodes(); + public SessionTypes SessionType = new SessionTypes(); + public SignatureAlgorithms SignatureAlgorithm = new SignatureAlgorithms(); + public Modes Mode = new Modes(); + public IsValidValues IsValid = new IsValidValues(); + + internal sealed class ErrorCodes { + public string UnsupportedType = "unsupported-type"; + } + internal sealed class SessionTypes { + /// <summary> + /// A preference order list of all supported session types. + /// </summary> + public string[] All { get { return new[] { DH_SHA512, DH_SHA384, DH_SHA256, DH_SHA1, NoEncryption }; } } + public string[] AllDiffieHellman { get { return new[] { DH_SHA512, DH_SHA384, DH_SHA256, DH_SHA1 }; } } + public string DH_SHA1 = "DH-SHA1"; + public string DH_SHA256; + public string DH_SHA384; + public string DH_SHA512; + public string NoEncryption = string.Empty; + public string Best { + get { + foreach (string algorithmName in All) { + if (algorithmName != null) { + return algorithmName; + } + } + throw new ProtocolException(); // really bad... we have no signing algorithms at all + } + } + } + internal sealed class SignatureAlgorithms { + /// <summary> + /// A preference order list of signature algorithms we support. + /// </summary> + public string[] All { get { return new[] { HMAC_SHA512, HMAC_SHA384, HMAC_SHA256, HMAC_SHA1 }; } } + public string HMAC_SHA1 = "HMAC-SHA1"; + public string HMAC_SHA256; + public string HMAC_SHA384; + public string HMAC_SHA512; + public string Best { + get { + foreach (string algorithmName in All) { + if (algorithmName != null) { + return algorithmName; + } + } + throw new ProtocolException(); // really bad... we have no signing algorithms at all + } + } + } + internal sealed class Modes { + public string cancel = "cancel"; + public string error = "error"; + public string id_res = "id_res"; + public string checkid_immediate = "checkid_immediate"; + public string checkid_setup = "checkid_setup"; + public string check_authentication = "check_authentication"; + public string associate = "associate"; + public string setup_needed = "id_res"; // V2 overrides this + } + internal sealed class IsValidValues { + public string True = "true"; + public string False = "false"; + } + } + + /// <summary> + /// The maximum time a user can be allowed to take to complete authentication. + /// </summary> + /// <remarks> + /// This is used to calculate the length of time that nonces are stored. + /// This is internal until we can decide whether to leave this static, or make + /// it an instance member, or put it inside the IConsumerApplicationStore interface. + /// </remarks> + internal static TimeSpan MaximumUserAgentAuthenticationTime = TimeSpan.FromMinutes(5); + /// <summary> + /// The maximum permissible difference in clocks between relying party and + /// provider web servers, discounting time zone differences. + /// </summary> + /// <remarks> + /// This is used when storing/validating nonces from the provider. + /// If it is conceivable that a server's clock could be up to five minutes + /// off from true UTC time, then the maximum time skew should be set to + /// ten minutes to allow one server to be five minutes ahead and the remote + /// server to be five minutes behind and still be able to communicate. + /// </remarks> + internal static TimeSpan MaximumAllowableTimeSkew = TimeSpan.FromMinutes(10); + + /// <summary> + /// Checks whether a given Protocol version practically equals this one + /// for purposes of verifying a match for assertion verification. + /// </summary> + /// <param name="other">The other version to check against this one.</param> + /// <returns><c>true</c> if this and the given Protocol versions are essentially the same.</returns> + /// <remarks> + /// OpenID v1.0 never had a spec, and 1.0 and 1.1 are indistinguishable because of that. + /// Therefore for assertion verification, 1.0 and 1.1 are considered equivalent. + /// </remarks> + public bool EqualsPractically(Protocol other) { + if (other == null) { + return false; + } + + // Exact version match is definitely equality. + if (this.Version == other.Version) { + return true; + } + + // If both protocol versions are 1.x, it doesn't matter if one + // is 1.0 and the other is 1.1 for assertion verification purposes. + if (this.Version.Major == 1 && other.Version.Major == 1) { + return true; + } + + // Different version. + return false; + } + + public override bool Equals(object obj) { + Protocol other = obj as Protocol; + if (other == null) { + return false; + } + + return this.Version == other.Version; + } + public override int GetHashCode() { + return Version.GetHashCode(); + } + public override string ToString() { + return string.Format(CultureInfo.CurrentCulture, "OpenID Authentication {0}.{1}", Version.Major, Version.Minor); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AnonymousRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AnonymousRequest.cs new file mode 100644 index 0000000..974d729 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AnonymousRequest.cs @@ -0,0 +1,92 @@ +//----------------------------------------------------------------------- +// <copyright file="AnonymousRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Provides access to a host Provider to read an incoming extension-only checkid request message, + /// and supply extension responses or a cancellation message to the RP. + /// </summary> + [Serializable] + internal class AnonymousRequest : HostProcessedRequest, IAnonymousRequest { + /// <summary> + /// The extension-response message to send, if the host site chooses to send it. + /// </summary> + private readonly IndirectSignedResponse positiveResponse; + + /// <summary> + /// Initializes a new instance of the <see cref="AnonymousRequest"/> class. + /// </summary> + /// <param name="provider">The provider that received the request.</param> + /// <param name="request">The incoming authentication request message.</param> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "AuthenticationRequest", Justification = "Type name"), SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code contracts require it.")] + internal AnonymousRequest(OpenIdProvider provider, SignedResponseRequest request) + : base(provider, request) { + Contract.Requires<ArgumentNullException>(provider != null); + Contract.Requires<ArgumentException>(!(request is CheckIdRequest)); + + this.positiveResponse = new IndirectSignedResponse(request); + } + + #region HostProcessedRequest members + + /// <summary> + /// Gets or sets the provider endpoint. + /// </summary> + /// <value> + /// The default value is the URL that the request came in on from the relying party. + /// </value> + public override Uri ProviderEndpoint { + get { return this.positiveResponse.ProviderEndpoint; } + set { this.positiveResponse.ProviderEndpoint = value; } + } + + #endregion + + #region IAnonymousRequest Members + + /// <summary> + /// Gets or sets a value indicating whether the user approved sending any data to the relying party. + /// </summary> + /// <value><c>true</c> if approved; otherwise, <c>false</c>.</value> + public bool? IsApproved { get; set; } + + #endregion + + #region Request members + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + public override bool IsResponseReady { + get { return this.IsApproved.HasValue; } + } + + /// <summary> + /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>. + /// </summary> + protected override IProtocolMessage ResponseMessage { + get { + if (this.IsApproved.HasValue) { + return this.IsApproved.Value ? (IProtocolMessage)this.positiveResponse : this.NegativeResponse; + } else { + return null; + } + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AnonymousRequestEventArgs.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AnonymousRequestEventArgs.cs new file mode 100644 index 0000000..9ffaa55 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AnonymousRequestEventArgs.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------- +// <copyright file="AnonymousRequestEventArgs.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The event arguments that include details of the incoming request. + /// </summary> + public class AnonymousRequestEventArgs : EventArgs { + /// <summary> + /// Initializes a new instance of the <see cref="AnonymousRequestEventArgs"/> class. + /// </summary> + /// <param name="request">The incoming OpenID request.</param> + internal AnonymousRequestEventArgs(IAnonymousRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + this.Request = request; + } + + /// <summary> + /// Gets the incoming OpenID request. + /// </summary> + public IAnonymousRequest Request { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AssociationDataBag.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AssociationDataBag.cs new file mode 100644 index 0000000..a8ac41e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AssociationDataBag.cs @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationDataBag.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.IO; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// A signed and encrypted serialization of an association. + /// </summary> + internal class AssociationDataBag : DataBag, IStreamSerializingDataBag { + /// <summary> + /// Initializes a new instance of the <see cref="AssociationDataBag"/> class. + /// </summary> + public AssociationDataBag() { + } + + /// <summary> + /// Gets or sets the association secret. + /// </summary> + [MessagePart(IsRequired = true)] + internal byte[] Secret { get; set; } + + /// <summary> + /// Gets or sets the UTC time that this association expires. + /// </summary> + [MessagePart(IsRequired = true)] + internal DateTime ExpiresUtc { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is for "dumb" mode RPs. + /// </summary> + /// <value> + /// <c>true</c> if this instance is private association; otherwise, <c>false</c>. + /// </value> + [MessagePart(IsRequired = true)] + internal bool IsPrivateAssociation { + get { return this.AssociationType == AssociationRelyingPartyType.Dumb; } + set { this.AssociationType = value ? AssociationRelyingPartyType.Dumb : AssociationRelyingPartyType.Smart; } + } + + /// <summary> + /// Gets or sets the type of the association (shared or private, a.k.a. smart or dumb). + /// </summary> + internal AssociationRelyingPartyType AssociationType { get; set; } + + /// <summary> + /// Serializes the instance to the specified stream. + /// </summary> + /// <param name="stream">The stream.</param> + public void Serialize(Stream stream) { + var writer = new BinaryWriter(stream); + writer.Write(this.IsPrivateAssociation); + writer.WriteBuffer(this.Secret); + writer.Write((int)(this.ExpiresUtc - TimestampEncoder.Epoch).TotalSeconds); + writer.Flush(); + } + + /// <summary> + /// Initializes the fields on this instance from the specified stream. + /// </summary> + /// <param name="stream">The stream.</param> + public void Deserialize(Stream stream) { + var reader = new BinaryReader(stream); + this.IsPrivateAssociation = reader.ReadBoolean(); + this.Secret = reader.ReadBuffer(); + this.ExpiresUtc = TimestampEncoder.Epoch + TimeSpan.FromSeconds(reader.ReadInt32()); + } + + /// <summary> + /// Creates the formatter used for serialization of this type. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store used when signing or encrypting.</param> + /// <param name="bucket">The bucket in which symmetric keys are stored for signing/encrypting data.</param> + /// <param name="minimumAge">The minimum age.</param> + /// <returns> + /// A formatter for serialization. + /// </returns> + internal static IDataBagFormatter<AssociationDataBag> CreateFormatter(ICryptoKeyStore cryptoKeyStore, string bucket, TimeSpan? minimumAge = null) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(bucket)); + Contract.Ensures(Contract.Result<IDataBagFormatter<AssociationDataBag>>() != null); + return new BinaryDataBagFormatter<AssociationDataBag>(cryptoKeyStore, bucket, signed: true, encrypted: true, minimumAge: minimumAge); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AssociationRelyingPartyType.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AssociationRelyingPartyType.cs new file mode 100644 index 0000000..4d121b1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AssociationRelyingPartyType.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// <copyright file="AssociationRelyingPartyType.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + /// <summary> + /// An enumeration that can specify how a given <see cref="Association"/> is used. + /// </summary> + public enum AssociationRelyingPartyType { + /// <summary> + /// The <see cref="Association"/> manages a shared secret between + /// Provider and Relying Party sites that allows the RP to verify + /// the signature on a message from an OP. + /// </summary> + Smart, + + /// <summary> + /// The <see cref="Association"/> manages a secret known alone by + /// a Provider that allows the Provider to verify its own signatures + /// for "dumb" (stateless) relying parties. + /// </summary> + Dumb + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AuthenticationChallengeEventArgs.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AuthenticationChallengeEventArgs.cs new file mode 100644 index 0000000..1594994 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AuthenticationChallengeEventArgs.cs @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationChallengeEventArgs.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + + /// <summary> + /// The event arguments that include details of the incoming request. + /// </summary> + public class AuthenticationChallengeEventArgs : EventArgs { + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationChallengeEventArgs"/> class. + /// </summary> + /// <param name="request">The incoming authentication request.</param> + internal AuthenticationChallengeEventArgs(IAuthenticationRequest request) { + this.Request = request; + } + + /// <summary> + /// Gets the incoming authentication request. + /// </summary> + public IAuthenticationRequest Request { get; internal set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AuthenticationRequest.cs new file mode 100644 index 0000000..d22f858 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AuthenticationRequest.cs @@ -0,0 +1,227 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Implements the <see cref="IAuthenticationRequest"/> interface + /// so that OpenID Provider sites can easily respond to authentication + /// requests. + /// </summary> + [Serializable] + internal class AuthenticationRequest : HostProcessedRequest, IAuthenticationRequest { + /// <summary> + /// The positive assertion to send, if the host site chooses to send it. + /// </summary> + private readonly PositiveAssertionResponse positiveResponse; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class. + /// </summary> + /// <param name="provider">The provider that received the request.</param> + /// <param name="request">The incoming authentication request message.</param> + internal AuthenticationRequest(OpenIdProvider provider, CheckIdRequest request) + : base(provider, request) { + Contract.Requires<ArgumentNullException>(provider != null); + + this.positiveResponse = new PositiveAssertionResponse(request); + + if (this.ClaimedIdentifier == Protocol.ClaimedIdentifierForOPIdentifier && + Protocol.ClaimedIdentifierForOPIdentifier != null) { + // Force the hosting OP to deal with identifier_select by nulling out the two identifiers. + this.IsDirectedIdentity = true; + this.positiveResponse.ClaimedIdentifier = null; + this.positiveResponse.LocalIdentifier = null; + } + + // URL delegation is only detectable from 2.0 RPs, since openid.claimed_id isn't included from 1.0 RPs. + // If the openid.claimed_id is present, and if it's different than the openid.identity argument, then + // the RP has discovered a claimed identifier that has delegated authentication to this Provider. + this.IsDelegatedIdentifier = this.ClaimedIdentifier != null && this.ClaimedIdentifier != this.LocalIdentifier; + + Reporting.RecordEventOccurrence("AuthenticationRequest.IsDelegatedIdentifier", this.IsDelegatedIdentifier.ToString()); + } + + #region HostProcessedRequest members + + /// <summary> + /// Gets or sets the provider endpoint. + /// </summary> + /// <value> + /// The default value is the URL that the request came in on from the relying party. + /// </value> + public override Uri ProviderEndpoint { + get { return this.positiveResponse.ProviderEndpoint; } + set { this.positiveResponse.ProviderEndpoint = value; } + } + + #endregion + + /// <summary> + /// Gets a value indicating whether the response is ready to be created and sent. + /// </summary> + public override bool IsResponseReady { + get { + // The null checks on the identifiers is to make sure that an identifier_select + // has been resolved to actual identifiers. + return this.IsAuthenticated.HasValue && + (!this.IsAuthenticated.Value || !this.IsDirectedIdentity || (this.LocalIdentifier != null && this.ClaimedIdentifier != null)); + } + } + + #region IAuthenticationRequest Properties + + /// <summary> + /// Gets a value indicating whether the Provider should help the user + /// select a Claimed Identifier to send back to the relying party. + /// </summary> + public bool IsDirectedIdentity { get; private set; } + + /// <summary> + /// Gets a value indicating whether the requesting Relying Party is using a delegated URL. + /// </summary> + /// <remarks> + /// When delegated identifiers are used, the <see cref="ClaimedIdentifier"/> should not + /// be changed at the Provider during authentication. + /// Delegation is only detectable on requests originating from OpenID 2.0 relying parties. + /// A relying party implementing only OpenID 1.x may use delegation and this property will + /// return false anyway. + /// </remarks> + public bool IsDelegatedIdentifier { get; private set; } + + /// <summary> + /// Gets or sets the Local Identifier to this OpenID Provider of the user attempting + /// to authenticate. Check <see cref="IsDirectedIdentity"/> to see if + /// this value is valid. + /// </summary> + /// <remarks> + /// This may or may not be the same as the Claimed Identifier that the user agent + /// originally supplied to the relying party. The Claimed Identifier + /// endpoint may be delegating authentication to this provider using + /// this provider's local id, which is what this property contains. + /// Use this identifier when looking up this user in the provider's user account + /// list. + /// </remarks> + public Identifier LocalIdentifier { + get { + return this.positiveResponse.LocalIdentifier; + } + + set { + // Keep LocalIdentifier and ClaimedIdentifier in sync for directed identity. + if (this.IsDirectedIdentity) { + if (this.ClaimedIdentifier != null && this.ClaimedIdentifier != value) { + throw new InvalidOperationException(OpenIdStrings.IdentifierSelectRequiresMatchingIdentifiers); + } + + this.positiveResponse.ClaimedIdentifier = value; + } + + this.positiveResponse.LocalIdentifier = value; + } + } + + /// <summary> + /// Gets or sets the identifier that the user agent is claiming at the relying party site. + /// Check <see cref="IsDirectedIdentity"/> to see if this value is valid. + /// </summary> + /// <remarks> + /// <para>This property can only be set if <see cref="IsDelegatedIdentifier"/> is + /// false, to prevent breaking URL delegation.</para> + /// <para>This will not be the same as this provider's local identifier for the user + /// if the user has set up his/her own identity page that points to this + /// provider for authentication.</para> + /// <para>The provider may use this identifier for displaying to the user when + /// asking for the user's permission to authenticate to the relying party.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown from the setter + /// if <see cref="IsDelegatedIdentifier"/> is true.</exception> + public Identifier ClaimedIdentifier { + get { + return this.positiveResponse.ClaimedIdentifier; + } + + set { + // Keep LocalIdentifier and ClaimedIdentifier in sync for directed identity. + if (this.IsDirectedIdentity) { + this.positiveResponse.LocalIdentifier = value; + } + + this.positiveResponse.ClaimedIdentifier = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether the provider has determined that the + /// <see cref="ClaimedIdentifier"/> belongs to the currently logged in user + /// and wishes to share this information with the consumer. + /// </summary> + public bool? IsAuthenticated { get; set; } + + #endregion + + /// <summary> + /// Gets the original request message. + /// </summary> + protected new CheckIdRequest RequestMessage { + get { return (CheckIdRequest)base.RequestMessage; } + } + + /// <summary> + /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>. + /// </summary> + protected override IProtocolMessage ResponseMessage { + get { + if (this.IsAuthenticated.HasValue) { + return this.IsAuthenticated.Value ? (IProtocolMessage)this.positiveResponse : this.NegativeResponse; + } else { + return null; + } + } + } + + #region IAuthenticationRequest Methods + + /// <summary> + /// Adds an optional fragment (#fragment) portion to the ClaimedIdentifier. + /// Useful for identifier recycling. + /// </summary> + /// <param name="fragment">Should not include the # prefix character as that will be added internally. + /// May be null or the empty string to clear a previously set fragment.</param> + /// <remarks> + /// <para>Unlike the <see cref="ClaimedIdentifier"/> property, which can only be set if + /// using directed identity, this method can be called on any URI claimed identifier.</para> + /// <para>Because XRI claimed identifiers (the canonical IDs) are never recycled, + /// this method should<i>not</i> be called for XRIs.</para> + /// </remarks> + /// <exception cref="InvalidOperationException"> + /// Thrown when this method is called on an XRI, or on a directed identity + /// request before the <see cref="ClaimedIdentifier"/> property is set. + /// </exception> + public void SetClaimedIdentifierFragment(string fragment) { + UriBuilder builder = new UriBuilder(this.ClaimedIdentifier); + builder.Fragment = fragment; + this.positiveResponse.ClaimedIdentifier = builder.Uri; + } + + /// <summary> + /// Sets the Claimed and Local identifiers even after they have been initially set. + /// </summary> + /// <param name="identifier">The value to set to the <see cref="ClaimedIdentifier"/> and <see cref="LocalIdentifier"/> properties.</param> + internal void ResetClaimedAndLocalIdentifiers(Identifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + + this.positiveResponse.ClaimedIdentifier = identifier; + this.positiveResponse.LocalIdentifier = identifier; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/AutoResponsiveRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AutoResponsiveRequest.cs new file mode 100644 index 0000000..41e082b --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/AutoResponsiveRequest.cs @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------- +// <copyright file="AutoResponsiveRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Handles messages coming into an OpenID Provider for which the entire + /// response message can be automatically determined without help from + /// the hosting web site. + /// </summary> + internal class AutoResponsiveRequest : Request { + /// <summary> + /// The response message to send. + /// </summary> + private readonly IProtocolMessage response; + + /// <summary> + /// Initializes a new instance of the <see cref="AutoResponsiveRequest"/> class. + /// </summary> + /// <param name="request">The request message.</param> + /// <param name="response">The response that is ready for transmittal.</param> + /// <param name="securitySettings">The security settings.</param> + internal AutoResponsiveRequest(IDirectedProtocolMessage request, IProtocolMessage response, ProviderSecuritySettings securitySettings) + : base(request, securitySettings) { + Contract.Requires<ArgumentNullException>(response != null); + + this.response = response; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AutoResponsiveRequest"/> class + /// for a response to an unrecognizable request. + /// </summary> + /// <param name="response">The response that is ready for transmittal.</param> + /// <param name="securitySettings">The security settings.</param> + internal AutoResponsiveRequest(IProtocolMessage response, ProviderSecuritySettings securitySettings) + : base(IndirectResponseBase.GetVersion(response), securitySettings) { + Contract.Requires<ArgumentNullException>(response != null); + + this.response = response; + } + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + public override bool IsResponseReady { + get { return true; } + } + + /// <summary> + /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>. + /// </summary> + internal IProtocolMessage ResponseMessageTestHook { + get { return this.ResponseMessage; } + } + + /// <summary> + /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>. + /// </summary> + protected override IProtocolMessage ResponseMessage { + get { return this.response; } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/HostProcessedRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/HostProcessedRequest.cs new file mode 100644 index 0000000..ec0c58a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/HostProcessedRequest.cs @@ -0,0 +1,178 @@ +//----------------------------------------------------------------------- +// <copyright file="HostProcessedRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Net; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A base class from which identity and non-identity RP requests can derive. + /// </summary> + [Serializable] + internal abstract class HostProcessedRequest : Request, IHostProcessedRequest { + /// <summary> + /// The negative assertion to send, if the host site chooses to send it. + /// </summary> + private readonly NegativeAssertionResponse negativeResponse; + + /// <summary> + /// A cache of the result from discovery of the Realm URL. + /// </summary> + private RelyingPartyDiscoveryResult? realmDiscoveryResult; + + /// <summary> + /// Initializes a new instance of the <see cref="HostProcessedRequest"/> class. + /// </summary> + /// <param name="provider">The provider that received the request.</param> + /// <param name="request">The incoming request message.</param> + protected HostProcessedRequest(OpenIdProvider provider, SignedResponseRequest request) + : base(request, provider.SecuritySettings) { + Contract.Requires<ArgumentNullException>(provider != null); + + this.negativeResponse = new NegativeAssertionResponse(request, provider.Channel); + Reporting.RecordEventOccurrence(this, request.Realm); + } + + #region IHostProcessedRequest Properties + + /// <summary> + /// Gets the version of OpenID being used by the relying party that sent the request. + /// </summary> + public ProtocolVersion RelyingPartyVersion { + get { return Protocol.Lookup(this.RequestMessage.Version).ProtocolVersion; } + } + + /// <summary> + /// Gets a value indicating whether the consumer demands an immediate response. + /// If false, the consumer is willing to wait for the identity provider + /// to authenticate the user. + /// </summary> + public bool Immediate { + get { return this.RequestMessage.Immediate; } + } + + /// <summary> + /// Gets the URL the consumer site claims to use as its 'base' address. + /// </summary> + public Realm Realm { + get { return this.RequestMessage.Realm; } + } + + /// <summary> + /// Gets or sets the provider endpoint. + /// </summary> + /// <value> + /// The default value is the URL that the request came in on from the relying party. + /// </value> + public abstract Uri ProviderEndpoint { get; set; } + + #endregion + + /// <summary> + /// Gets a value indicating whether realm discovery been performed. + /// </summary> + internal bool HasRealmDiscoveryBeenPerformed { + get { return this.realmDiscoveryResult.HasValue; } + } + + /// <summary> + /// Gets the negative response. + /// </summary> + protected NegativeAssertionResponse NegativeResponse { + get { return this.negativeResponse; } + } + + /// <summary> + /// Gets the original request message. + /// </summary> + /// <value>This may be null in the case of an unrecognizable message.</value> + protected new SignedResponseRequest RequestMessage { + get { return (SignedResponseRequest)base.RequestMessage; } + } + + #region IHostProcessedRequest Methods + + /// <summary> + /// Gets a value indicating whether verification of the return URL claimed by the Relying Party + /// succeeded. + /// </summary> + /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param> + /// <returns>Result of realm discovery.</returns> + /// <remarks> + /// Return URL verification is only attempted if this property is queried. + /// The result of the verification is cached per request so calling this + /// property getter multiple times in one request is not a performance hit. + /// See OpenID Authentication 2.0 spec section 9.2.1. + /// </remarks> + public RelyingPartyDiscoveryResult IsReturnUrlDiscoverable(OpenIdProvider provider) { + if (!this.realmDiscoveryResult.HasValue) { + this.realmDiscoveryResult = this.IsReturnUrlDiscoverableCore(provider); + } + + return this.realmDiscoveryResult.Value; + } + + /// <summary> + /// Gets a value indicating whether verification of the return URL claimed by the Relying Party + /// succeeded. + /// </summary> + /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param> + /// <returns>Result of realm discovery.</returns> + private RelyingPartyDiscoveryResult IsReturnUrlDiscoverableCore(OpenIdProvider provider) { + Contract.Requires<ArgumentNullException>(provider != null); + + ErrorUtilities.VerifyInternal(this.Realm != null, "Realm should have been read or derived by now."); + + try { + if (this.SecuritySettings.RequireSsl && this.Realm.Scheme != Uri.UriSchemeHttps) { + Logger.OpenId.WarnFormat("RP discovery failed because RequireSsl is true and RP discovery would begin at insecure URL {0}.", this.Realm); + return RelyingPartyDiscoveryResult.NoServiceDocument; + } + + var returnToEndpoints = this.Realm.DiscoverReturnToEndpoints(provider.Channel.WebRequestHandler, false); + if (returnToEndpoints == null) { + return RelyingPartyDiscoveryResult.NoServiceDocument; + } + + foreach (var returnUrl in returnToEndpoints) { + Realm discoveredReturnToUrl = returnUrl.ReturnToEndpoint; + + // The spec requires that the return_to URLs given in an RPs XRDS doc + // do not contain wildcards. + if (discoveredReturnToUrl.DomainWildcard) { + Logger.Yadis.WarnFormat("Realm {0} contained return_to URL {1} which contains a wildcard, which is not allowed.", Realm, discoveredReturnToUrl); + continue; + } + + // Use the same rules as return_to/realm matching to check whether this + // URL fits the return_to URL we were given. + if (discoveredReturnToUrl.Contains(this.RequestMessage.ReturnTo)) { + // no need to keep looking after we find a match + return RelyingPartyDiscoveryResult.Success; + } + } + } catch (ProtocolException ex) { + // Don't do anything else. We quietly fail at return_to verification and return false. + Logger.Yadis.InfoFormat("Relying party discovery at URL {0} failed. {1}", Realm, ex); + return RelyingPartyDiscoveryResult.NoServiceDocument; + } catch (WebException ex) { + // Don't do anything else. We quietly fail at return_to verification and return false. + Logger.Yadis.InfoFormat("Relying party discovery at URL {0} failed. {1}", Realm, ex); + return RelyingPartyDiscoveryResult.NoServiceDocument; + } + + return RelyingPartyDiscoveryResult.NoMatchingReturnTo; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IAnonymousRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IAnonymousRequest.cs new file mode 100644 index 0000000..ec2c175 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IAnonymousRequest.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------- +// <copyright file="IAnonymousRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// Instances of this interface represent incoming extension-only requests. + /// This interface provides the details of the request and allows setting + /// the response. + /// </summary> + public interface IAnonymousRequest : IHostProcessedRequest { + /// <summary> + /// Gets or sets a value indicating whether the user approved sending any data to the relying party. + /// </summary> + /// <value><c>true</c> if approved; otherwise, <c>false</c>.</value> + bool? IsApproved { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IAuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IAuthenticationRequest.cs new file mode 100644 index 0000000..f59d436 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IAuthenticationRequest.cs @@ -0,0 +1,367 @@ +//----------------------------------------------------------------------- +// <copyright file="IAuthenticationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Instances of this interface represent incoming authentication requests. + /// This interface provides the details of the request and allows setting + /// the response. + /// </summary> + [ContractClass(typeof(IAuthenticationRequestContract))] + public interface IAuthenticationRequest : IHostProcessedRequest { + /// <summary> + /// Gets a value indicating whether the Provider should help the user + /// select a Claimed Identifier to send back to the relying party. + /// </summary> + bool IsDirectedIdentity { get; } + + /// <summary> + /// Gets a value indicating whether the requesting Relying Party is using a delegated URL. + /// </summary> + /// <remarks> + /// When delegated identifiers are used, the <see cref="ClaimedIdentifier"/> should not + /// be changed at the Provider during authentication. + /// Delegation is only detectable on requests originating from OpenID 2.0 relying parties. + /// A relying party implementing only OpenID 1.x may use delegation and this property will + /// return false anyway. + /// </remarks> + bool IsDelegatedIdentifier { get; } + + /// <summary> + /// Gets or sets the Local Identifier to this OpenID Provider of the user attempting + /// to authenticate. Check <see cref="IsDirectedIdentity"/> to see if + /// this value is valid. + /// </summary> + /// <remarks> + /// This may or may not be the same as the Claimed Identifier that the user agent + /// originally supplied to the relying party. The Claimed Identifier + /// endpoint may be delegating authentication to this provider using + /// this provider's local id, which is what this property contains. + /// Use this identifier when looking up this user in the provider's user account + /// list. + /// </remarks> + Identifier LocalIdentifier { get; set; } + + /// <summary> + /// Gets or sets the identifier that the user agent is claiming at the relying party site. + /// Check <see cref="IsDirectedIdentity"/> to see if this value is valid. + /// </summary> + /// <remarks> + /// <para>This property can only be set if <see cref="IsDelegatedIdentifier"/> is + /// false, to prevent breaking URL delegation.</para> + /// <para>This will not be the same as this provider's local identifier for the user + /// if the user has set up his/her own identity page that points to this + /// provider for authentication.</para> + /// <para>The provider may use this identifier for displaying to the user when + /// asking for the user's permission to authenticate to the relying party.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown from the setter + /// if <see cref="IsDelegatedIdentifier"/> is true.</exception> + Identifier ClaimedIdentifier { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the provider has determined that the + /// <see cref="ClaimedIdentifier"/> belongs to the currently logged in user + /// and wishes to share this information with the consumer. + /// </summary> + bool? IsAuthenticated { get; set; } + + /// <summary> + /// Adds an optional fragment (#fragment) portion to the ClaimedIdentifier. + /// Useful for identifier recycling. + /// </summary> + /// <param name="fragment"> + /// Should not include the # prefix character as that will be added internally. + /// May be null or the empty string to clear a previously set fragment. + /// </param> + /// <remarks> + /// <para>Unlike the <see cref="ClaimedIdentifier"/> property, which can only be set if + /// using directed identity, this method can be called on any URI claimed identifier.</para> + /// <para>Because XRI claimed identifiers (the canonical IDs) are never recycled, + /// this method should<i>not</i> be called for XRIs.</para> + /// </remarks> + /// <exception cref="InvalidOperationException"> + /// Thrown when this method is called on an XRI, or on a directed identity + /// request before the <see cref="ClaimedIdentifier"/> property is set. + /// </exception> + void SetClaimedIdentifierFragment(string fragment); + } + + /// <summary> + /// Code contract class for the <see cref="IAuthenticationRequest"/> type. + /// </summary> + [ContractClassFor(typeof(IAuthenticationRequest))] + internal abstract class IAuthenticationRequestContract : IAuthenticationRequest { + /// <summary> + /// Initializes a new instance of the <see cref="IAuthenticationRequestContract"/> class. + /// </summary> + protected IAuthenticationRequestContract() { + } + + #region IAuthenticationRequest Properties + + /// <summary> + /// Gets a value indicating whether the Provider should help the user + /// select a Claimed Identifier to send back to the relying party. + /// </summary> + bool IAuthenticationRequest.IsDirectedIdentity { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether the requesting Relying Party is using a delegated URL. + /// </summary> + /// <remarks> + /// When delegated identifiers are used, the <see cref="IAuthenticationRequest.ClaimedIdentifier"/> should not + /// be changed at the Provider during authentication. + /// Delegation is only detectable on requests originating from OpenID 2.0 relying parties. + /// A relying party implementing only OpenID 1.x may use delegation and this property will + /// return false anyway. + /// </remarks> + bool IAuthenticationRequest.IsDelegatedIdentifier { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets or sets the Local Identifier to this OpenID Provider of the user attempting + /// to authenticate. Check <see cref="IAuthenticationRequest.IsDirectedIdentity"/> to see if + /// this value is valid. + /// </summary> + /// <remarks> + /// This may or may not be the same as the Claimed Identifier that the user agent + /// originally supplied to the relying party. The Claimed Identifier + /// endpoint may be delegating authentication to this provider using + /// this provider's local id, which is what this property contains. + /// Use this identifier when looking up this user in the provider's user account + /// list. + /// </remarks> + Identifier IAuthenticationRequest.LocalIdentifier { + get { + throw new NotImplementedException(); + } + + set { + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets or sets the identifier that the user agent is claiming at the relying party site. + /// Check <see cref="IAuthenticationRequest.IsDirectedIdentity"/> to see if this value is valid. + /// </summary> + /// <remarks> + /// <para>This property can only be set if <see cref="IAuthenticationRequest.IsDelegatedIdentifier"/> is + /// false, to prevent breaking URL delegation.</para> + /// <para>This will not be the same as this provider's local identifier for the user + /// if the user has set up his/her own identity page that points to this + /// provider for authentication.</para> + /// <para>The provider may use this identifier for displaying to the user when + /// asking for the user's permission to authenticate to the relying party.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown from the setter + /// if <see cref="IAuthenticationRequest.IsDelegatedIdentifier"/> is true.</exception> + Identifier IAuthenticationRequest.ClaimedIdentifier { + get { + throw new NotImplementedException(); + } + + set { + IAuthenticationRequest req = this; + Contract.Requires<InvalidOperationException>(!req.IsDelegatedIdentifier, OpenIdStrings.ClaimedIdentifierCannotBeSetOnDelegatedAuthentication); + Contract.Requires<InvalidOperationException>(!req.IsDirectedIdentity || !(req.LocalIdentifier != null && req.LocalIdentifier != value), OpenIdStrings.IdentifierSelectRequiresMatchingIdentifiers); + } + } + + /// <summary> + /// Gets or sets a value indicating whether the provider has determined that the + /// <see cref="IAuthenticationRequest.ClaimedIdentifier"/> belongs to the currently logged in user + /// and wishes to share this information with the consumer. + /// </summary> + bool? IAuthenticationRequest.IsAuthenticated { + get { + throw new NotImplementedException(); + } + + set { + throw new NotImplementedException(); + } + } + + #endregion + + #region IHostProcessedRequest Properties + + /// <summary> + /// Gets the version of OpenID being used by the relying party that sent the request. + /// </summary> + ProtocolVersion IHostProcessedRequest.RelyingPartyVersion { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the URL the consumer site claims to use as its 'base' address. + /// </summary> + Realm IHostProcessedRequest.Realm { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether the consumer demands an immediate response. + /// If false, the consumer is willing to wait for the identity provider + /// to authenticate the user. + /// </summary> + bool IHostProcessedRequest.Immediate { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets or sets the provider endpoint claimed in the positive assertion. + /// </summary> + /// <value> + /// The default value is the URL that the request came in on from the relying party. + /// This value MUST match the value for the OP Endpoint in the discovery results for the + /// claimed identifier being asserted in a positive response. + /// </value> + Uri IHostProcessedRequest.ProviderEndpoint { + get { + throw new NotImplementedException(); + } + + set { + throw new NotImplementedException(); + } + } + + #endregion + + #region IRequest Properties + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + bool IRequest.IsResponseReady { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets or sets the security settings that apply to this request. + /// </summary> + /// <value> + /// Defaults to the <see cref="OpenIdProvider.SecuritySettings"/> on the <see cref="OpenIdProvider"/>. + /// </value> + ProviderSecuritySettings IRequest.SecuritySettings { + get { + throw new NotImplementedException(); + } + + set { + throw new NotImplementedException(); + } + } + + #endregion + + #region IAuthenticationRequest Methods + + /// <summary> + /// Adds an optional fragment (#fragment) portion to the ClaimedIdentifier. + /// Useful for identifier recycling. + /// </summary> + /// <param name="fragment">Should not include the # prefix character as that will be added internally. + /// May be null or the empty string to clear a previously set fragment.</param> + /// <remarks> + /// <para>Unlike the <see cref="IAuthenticationRequest.ClaimedIdentifier"/> property, which can only be set if + /// using directed identity, this method can be called on any URI claimed identifier.</para> + /// <para>Because XRI claimed identifiers (the canonical IDs) are never recycled, + /// this method should<i>not</i> be called for XRIs.</para> + /// </remarks> + /// <exception cref="InvalidOperationException"> + /// Thrown when this method is called on an XRI, or on a directed identity + /// request before the <see cref="IAuthenticationRequest.ClaimedIdentifier"/> property is set. + /// </exception> + void IAuthenticationRequest.SetClaimedIdentifierFragment(string fragment) { + Contract.Requires<InvalidOperationException>(!(((IAuthenticationRequest)this).IsDirectedIdentity && ((IAuthenticationRequest)this).ClaimedIdentifier == null), OpenIdStrings.ClaimedIdentifierMustBeSetFirst); + Contract.Requires<InvalidOperationException>(!(((IAuthenticationRequest)this).ClaimedIdentifier is XriIdentifier), OpenIdStrings.FragmentNotAllowedOnXRIs); + + throw new NotImplementedException(); + } + + #endregion + + #region IHostProcessedRequest Methods + + /// <summary> + /// Attempts to perform relying party discovery of the return URL claimed by the Relying Party. + /// </summary> + /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param> + /// <returns> + /// The details of how successful the relying party discovery was. + /// </returns> + /// <remarks> + /// <para>Return URL verification is only attempted if this method is called.</para> + /// <para>See OpenID Authentication 2.0 spec section 9.2.1.</para> + /// </remarks> + RelyingPartyDiscoveryResult IHostProcessedRequest.IsReturnUrlDiscoverable(OpenIdProvider provider) { + throw new NotImplementedException(); + } + + #endregion + + #region IRequest Methods + + /// <summary> + /// Adds an extension to the response to send to the relying party. + /// </summary> + /// <param name="extension">The extension to add to the response message.</param> + void IRequest.AddResponseExtension(DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension extension) { + throw new NotImplementedException(); + } + + /// <summary> + /// Removes any response extensions previously added using <see cref="IRequest.AddResponseExtension"/>. + /// </summary> + /// <remarks> + /// This should be called before sending a negative response back to the relying party + /// if extensions were already added, since negative responses cannot carry extensions. + /// </remarks> + void IRequest.ClearResponseExtensions() { + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <typeparam name="T">The type of the extension.</typeparam> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + T IRequest.GetExtension<T>() { + throw new NotImplementedException(); + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <param name="extensionType">The type of the extension.</param> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension IRequest.GetExtension(Type extensionType) { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs new file mode 100644 index 0000000..985bb54 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IDirectedIdentityIdentifierProvider.cs @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------- +// <copyright file="IDirectedIdentityIdentifierProvider.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + + /// <summary> + /// An interface to provide custom identifiers for users logging into specific relying parties. + /// </summary> + /// <remarks> + /// This interface would allow, for example, the Provider to offer PPIDs to their users, + /// allowing the users to log into RPs without leaving any clue as to their true identity, + /// and preventing multiple RPs from colluding to track user activity across realms. + /// </remarks> + [ContractClass(typeof(IDirectedIdentityIdentifierProviderContract))] + public interface IDirectedIdentityIdentifierProvider { + /// <summary> + /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of + /// an outgoing positive assertion. + /// </summary> + /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param> + /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param> + /// <returns> + /// A valid, discoverable OpenID Identifier that should be used as the value for the + /// openid.claimed_id and openid.local_id parameters. Must not be null. + /// </returns> + Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm); + + /// <summary> + /// Determines whether a given identifier is the primary (non-PPID) local identifier for some user. + /// </summary> + /// <param name="identifier">The identifier in question.</param> + /// <returns> + /// <c>true</c> if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, <c>false</c>. + /// </returns> + [Pure] + bool IsUserLocalIdentifier(Identifier identifier); + } + + /// <summary> + /// Contract class for the <see cref="IDirectedIdentityIdentifierProvider"/> type. + /// </summary> + [ContractClassFor(typeof(IDirectedIdentityIdentifierProvider))] + internal abstract class IDirectedIdentityIdentifierProviderContract : IDirectedIdentityIdentifierProvider { + #region IDirectedIdentityIdentifierProvider Members + + /// <summary> + /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of + /// an outgoing positive assertion. + /// </summary> + /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param> + /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param> + /// <returns> + /// A valid, discoverable OpenID Identifier that should be used as the value for the + /// openid.claimed_id and openid.local_id parameters. Must not be null. + /// </returns> + Uri IDirectedIdentityIdentifierProvider.GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) { + Contract.Requires<ArgumentNullException>(localIdentifier != null); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); + Contract.Requires<ArgumentException>(((IDirectedIdentityIdentifierProvider)this).IsUserLocalIdentifier(localIdentifier), OpenIdStrings.ArgumentIsPpidIdentifier); + throw new NotImplementedException(); + } + + /// <summary> + /// Determines whether a given identifier is the primary (non-PPID) local identifier for some user. + /// </summary> + /// <param name="identifier">The identifier in question.</param> + /// <returns> + /// <c>true</c> if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, <c>false</c>. + /// </returns> + bool IDirectedIdentityIdentifierProvider.IsUserLocalIdentifier(Identifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IErrorReporting.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IErrorReporting.cs new file mode 100644 index 0000000..1c73595 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IErrorReporting.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------- +// <copyright file="IErrorReporting.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An interface that a Provider site may implement in order to better + /// control error reporting. + /// </summary> + public interface IErrorReporting { + /// <summary> + /// Gets the message that can be sent in an error response + /// with information on who the remote party can contact + /// for help resolving the error. + /// </summary> + /// <value> + /// The contact address may take any form, as it is intended to be displayed to a person. + /// </value> + string Contact { get; } + + /// <summary> + /// Logs the details of an exception for later reference in diagnosing the problem. + /// </summary> + /// <param name="exception">The exception that was generated from the error.</param> + /// <returns> + /// A unique identifier for this particular error that the remote party can + /// reference when contacting <see cref="Contact"/> for help with this error. + /// May be null. + /// </returns> + /// <remarks> + /// The implementation of this method should never throw an unhandled exception + /// as that would preclude the ability to send the error response to the remote + /// party. When this method is not implemented, it should return null rather + /// than throwing <see cref="NotImplementedException"/>. + /// </remarks> + string LogError(ProtocolException exception); + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IHostProcessedRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IHostProcessedRequest.cs new file mode 100644 index 0000000..1c38d4b --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IHostProcessedRequest.cs @@ -0,0 +1,202 @@ +//----------------------------------------------------------------------- +// <copyright file="IHostProcessedRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Interface exposing incoming messages to the OpenID Provider that + /// require interaction with the host site. + /// </summary> + [ContractClass(typeof(IHostProcessedRequestContract))] + public interface IHostProcessedRequest : IRequest { + /// <summary> + /// Gets the version of OpenID being used by the relying party that sent the request. + /// </summary> + ProtocolVersion RelyingPartyVersion { get; } + + /// <summary> + /// Gets the URL the consumer site claims to use as its 'base' address. + /// </summary> + Realm Realm { get; } + + /// <summary> + /// Gets a value indicating whether the consumer demands an immediate response. + /// If false, the consumer is willing to wait for the identity provider + /// to authenticate the user. + /// </summary> + bool Immediate { get; } + + /// <summary> + /// Gets or sets the provider endpoint claimed in the positive assertion. + /// </summary> + /// <value> + /// The default value is the URL that the request came in on from the relying party. + /// This value MUST match the value for the OP Endpoint in the discovery results for the + /// claimed identifier being asserted in a positive response. + /// </value> + Uri ProviderEndpoint { get; set; } + + /// <summary> + /// Attempts to perform relying party discovery of the return URL claimed by the Relying Party. + /// </summary> + /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param> + /// <returns> + /// The details of how successful the relying party discovery was. + /// </returns> + /// <remarks> + /// <para>Return URL verification is only attempted if this method is called.</para> + /// <para>See OpenID Authentication 2.0 spec section 9.2.1.</para> + /// </remarks> + RelyingPartyDiscoveryResult IsReturnUrlDiscoverable(OpenIdProvider provider); + } + + /// <summary> + /// Code contract for the <see cref="IHostProcessedRequest"/> type. + /// </summary> + [ContractClassFor(typeof(IHostProcessedRequest))] + internal abstract class IHostProcessedRequestContract : IHostProcessedRequest { + /// <summary> + /// Initializes a new instance of the <see cref="IHostProcessedRequestContract"/> class. + /// </summary> + protected IHostProcessedRequestContract() { + } + + #region IHostProcessedRequest Properties + + /// <summary> + /// Gets the version of OpenID being used by the relying party that sent the request. + /// </summary> + ProtocolVersion IHostProcessedRequest.RelyingPartyVersion { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Gets the URL the consumer site claims to use as its 'base' address. + /// </summary> + Realm IHostProcessedRequest.Realm { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether the consumer demands an immediate response. + /// If false, the consumer is willing to wait for the identity provider + /// to authenticate the user. + /// </summary> + bool IHostProcessedRequest.Immediate { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Gets or sets the provider endpoint. + /// </summary> + /// <value> + /// The default value is the URL that the request came in on from the relying party. + /// </value> + Uri IHostProcessedRequest.ProviderEndpoint { + get { + Contract.Ensures(Contract.Result<Uri>() != null); + throw new NotImplementedException(); + } + + set { + Contract.Requires(value != null); + throw new NotImplementedException(); + } + } + + #endregion + + #region IRequest Members + + /// <summary> + /// Gets or sets the security settings that apply to this request. + /// </summary> + /// <value> + /// Defaults to the <see cref="OpenIdProvider.SecuritySettings"/> on the <see cref="OpenIdProvider"/>. + /// </value> + ProviderSecuritySettings IRequest.SecuritySettings { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + bool IRequest.IsResponseReady { + get { throw new System.NotImplementedException(); } + } + + /// <summary> + /// Adds an extension to the response to send to the relying party. + /// </summary> + /// <param name="extension">The extension to add to the response message.</param> + void IRequest.AddResponseExtension(DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension extension) { + throw new System.NotImplementedException(); + } + + /// <summary> + /// Removes any response extensions previously added using <see cref="IRequest.AddResponseExtension"/>. + /// </summary> + /// <remarks> + /// This should be called before sending a negative response back to the relying party + /// if extensions were already added, since negative responses cannot carry extensions. + /// </remarks> + void IRequest.ClearResponseExtensions() { + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <typeparam name="T">The type of the extension.</typeparam> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + T IRequest.GetExtension<T>() { + throw new System.NotImplementedException(); + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <param name="extensionType">The type of the extension.</param> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension IRequest.GetExtension(System.Type extensionType) { + throw new System.NotImplementedException(); + } + + #endregion + + #region IHostProcessedRequest Methods + + /// <summary> + /// Attempts to perform relying party discovery of the return URL claimed by the Relying Party. + /// </summary> + /// <param name="provider">The OpenIdProvider that is performing the RP discovery.</param> + /// <returns> + /// The details of how successful the relying party discovery was. + /// </returns> + /// <remarks> + /// <para>Return URL verification is only attempted if this method is called.</para> + /// <para>See OpenID Authentication 2.0 spec section 9.2.1.</para> + /// </remarks> + RelyingPartyDiscoveryResult IHostProcessedRequest.IsReturnUrlDiscoverable(OpenIdProvider provider) { + Contract.Requires<ArgumentNullException>(provider != null); + throw new System.NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IProviderAssociationStore.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IProviderAssociationStore.cs new file mode 100644 index 0000000..5a554c1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IProviderAssociationStore.cs @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------- +// <copyright file="IProviderAssociationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Provides association serialization and deserialization. + /// </summary> + /// <remarks> + /// Implementations may choose to store the association details in memory or a database table and simply return a + /// short, randomly generated string that is the key to that data. Alternatively, an implementation may + /// sign and encrypt the association details and then encode the results as a base64 string and return that value + /// as the association handle, thereby avoiding any association persistence at the OpenID Provider. + /// When taking the latter approach however, it is of course imperative that the association be encrypted + /// to avoid disclosing the secret to anyone who sees the association handle, which itself isn't considered to + /// be confidential. + /// </remarks> + [ContractClass(typeof(IProviderAssociationStoreContract))] + internal interface IProviderAssociationStore { + /// <summary> + /// Stores an association and returns a handle for it. + /// </summary> + /// <param name="secret">The association secret.</param> + /// <param name="expiresUtc">The UTC time that the association should expire.</param> + /// <param name="privateAssociation">A value indicating whether this is a private association.</param> + /// <returns> + /// The association handle that represents this association. + /// </returns> + string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation); + + /// <summary> + /// Retrieves an association given an association handle. + /// </summary> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="privateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). + /// </returns> + /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception> + Association Deserialize(IProtocolMessage containingMessage, bool privateAssociation, string handle); + } + + /// <summary> + /// Code contract for the <see cref="IProviderAssociationStore"/> interface. + /// </summary> + [ContractClassFor(typeof(IProviderAssociationStore))] + internal abstract class IProviderAssociationStoreContract : IProviderAssociationStore { + /// <summary> + /// Stores an association and returns a handle for it. + /// </summary> + /// <param name="secret">The association secret.</param> + /// <param name="expiresUtc">The expires UTC.</param> + /// <param name="privateAssociation">A value indicating whether this is a private association.</param> + /// <returns> + /// The association handle that represents this association. + /// </returns> + string IProviderAssociationStore.Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) { + Contract.Requires<ArgumentNullException>(secret != null); + Contract.Requires<ArgumentException>(expiresUtc.Kind == DateTimeKind.Utc); + Contract.Ensures(!String.IsNullOrEmpty(Contract.Result<string>())); + throw new NotImplementedException(); + } + + /// <summary> + /// Retrieves an association given an association handle. + /// </summary> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="privateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). + /// </returns> + /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception> + Association IProviderAssociationStore.Deserialize(IProtocolMessage containingMessage, bool privateAssociation, string handle) { + Contract.Requires<ArgumentNullException>(containingMessage != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(handle)); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IProviderBehavior.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IProviderBehavior.cs new file mode 100644 index 0000000..01b4ac8 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IProviderBehavior.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------- +// <copyright file="IProviderBehavior.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId.ChannelElements; + + /// <summary> + /// Applies a custom security policy to certain OpenID security settings and behaviors. + /// </summary> + [ContractClass(typeof(IProviderBehaviorContract))] + public interface IProviderBehavior { + /// <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(ProviderSecuritySettings securitySettings); + + /// <summary> + /// Called when a request is received by the Provider. + /// </summary> + /// <param name="request">The incoming request.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + /// <remarks> + /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but + /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/> + /// itself as that instance may be shared across many requests. + /// </remarks> + bool OnIncomingRequest(IRequest request); + + /// <summary> + /// Called when the Provider is preparing to send a response to an authentication request. + /// </summary> + /// <param name="request">The request that is configured to generate the outgoing response.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + bool OnOutgoingResponse(IAuthenticationRequest request); + } + + /// <summary> + /// Code contract for the <see cref="IProviderBehavior"/> type. + /// </summary> + [ContractClassFor(typeof(IProviderBehavior))] + internal abstract class IProviderBehaviorContract : IProviderBehavior { + /// <summary> + /// Initializes a new instance of the <see cref="IProviderBehaviorContract"/> class. + /// </summary> + protected IProviderBehaviorContract() { + } + + #region IProviderBehavior 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 IProviderBehavior.ApplySecuritySettings(ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(securitySettings != null); + throw new System.NotImplementedException(); + } + + /// <summary> + /// Called when a request is received by the Provider. + /// </summary> + /// <param name="request">The incoming request.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + /// <remarks> + /// Implementations may set a new value to <see cref="IRequest.SecuritySettings"/> but + /// should not change the properties on the instance of <see cref="ProviderSecuritySettings"/> + /// itself as that instance may be shared across many requests. + /// </remarks> + bool IProviderBehavior.OnIncomingRequest(IRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + throw new System.NotImplementedException(); + } + + /// <summary> + /// Called when the Provider is preparing to send a response to an authentication request. + /// </summary> + /// <param name="request">The request that is configured to generate the outgoing response.</param> + /// <returns> + /// <c>true</c> if this behavior owns this request and wants to stop other behaviors + /// from handling it; <c>false</c> to allow other behaviors to process this request. + /// </returns> + bool IProviderBehavior.OnOutgoingResponse(IAuthenticationRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + throw new System.NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IRequest.cs new file mode 100644 index 0000000..c231fa3 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IRequest.cs @@ -0,0 +1,151 @@ +//----------------------------------------------------------------------- +// <copyright file="IRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Represents an incoming OpenId authentication request. + /// </summary> + /// <remarks> + /// Requests may be infrastructural to OpenID and allow auto-responses, or they may + /// be authentication requests where the Provider site has to make decisions based + /// on its own user database and policies. + /// </remarks> + [ContractClass(typeof(IRequestContract))] + public interface IRequest { + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + bool IsResponseReady { get; } + + /// <summary> + /// Gets or sets the security settings that apply to this request. + /// </summary> + /// <value>Defaults to the <see cref="OpenIdProvider.SecuritySettings"/> on the <see cref="OpenIdProvider"/>.</value> + ProviderSecuritySettings SecuritySettings { get; set; } + + /// <summary> + /// Adds an extension to the response to send to the relying party. + /// </summary> + /// <param name="extension">The extension to add to the response message.</param> + void AddResponseExtension(IOpenIdMessageExtension extension); + + /// <summary> + /// Removes any response extensions previously added using <see cref="IRequest.AddResponseExtension"/>. + /// </summary> + /// <remarks> + /// This should be called before sending a negative response back to the relying party + /// if extensions were already added, since negative responses cannot carry extensions. + /// </remarks> + void ClearResponseExtensions(); + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <typeparam name="T">The type of the extension.</typeparam> + /// <returns>An instance of the extension initialized with values passed in with the request.</returns> + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "No parameter to make of type T.")] + T GetExtension<T>() where T : IOpenIdMessageExtension, new(); + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <param name="extensionType">The type of the extension.</param> + /// <returns>An instance of the extension initialized with values passed in with the request.</returns> + IOpenIdMessageExtension GetExtension(Type extensionType); + } + + /// <summary> + /// Code contract for the <see cref="IRequest"/> interface. + /// </summary> + [ContractClassFor(typeof(IRequest))] + internal abstract class IRequestContract : IRequest { + /// <summary> + /// Prevents a default instance of the <see cref="IRequestContract"/> class from being created. + /// </summary> + private IRequestContract() { + } + + #region IRequest Members + + /// <summary> + /// Gets or sets the security settings that apply to this request. + /// </summary> + /// <value> + /// Defaults to the <see cref="OpenIdProvider.SecuritySettings"/> on the <see cref="OpenIdProvider"/>. + /// </value> + ProviderSecuritySettings IRequest.SecuritySettings { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + bool IRequest.IsResponseReady { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Adds an extension to the response to send to the relying party. + /// </summary> + /// <param name="extension">The extension to add to the response message.</param> + void IRequest.AddResponseExtension(IOpenIdMessageExtension extension) { + Contract.Requires<ArgumentNullException>(extension != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Removes any response extensions previously added using <see cref="IRequest.AddResponseExtension"/>. + /// </summary> + /// <remarks> + /// This should be called before sending a negative response back to the relying party + /// if extensions were already added, since negative responses cannot carry extensions. + /// </remarks> + void IRequest.ClearResponseExtensions() { + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <typeparam name="T">The type of the extension.</typeparam> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + T IRequest.GetExtension<T>() { + throw new NotImplementedException(); + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <param name="extensionType">The type of the extension.</param> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + IOpenIdMessageExtension IRequest.GetExtension(Type extensionType) { + Contract.Requires<ArgumentNullException>(extensionType != null); + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IdentityEndpoint.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IdentityEndpoint.cs new file mode 100644 index 0000000..3a18b70 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IdentityEndpoint.cs @@ -0,0 +1,236 @@ +//----------------------------------------------------------------------- +// <copyright file="IdentityEndpoint.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Drawing.Design; + using System.Web.UI; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An ASP.NET control that manages the OpenID identity advertising tags + /// of a user's Identity Page that allow a relying party web site to discover + /// how to authenticate a user. + /// </summary> + [DefaultProperty("ServerUrl")] + [ToolboxData("<{0}:IdentityEndpoint runat=\"server\" ProviderEndpointUrl=\"\" />")] + public class IdentityEndpoint : XrdsPublisher { + #region Property viewstate keys + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="AutoNormalizeRequest"/> property. + /// </summary> + private const string AutoNormalizeRequestViewStateKey = "AutoNormalizeRequest"; + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="ProviderLocalIdentifier"/> property. + /// </summary> + private const string ProviderLocalIdentifierViewStateKey = "ProviderLocalIdentifier"; + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="ProviderVersion"/> property. + /// </summary> + private const string ProviderVersionViewStateKey = "ProviderVersion"; + + /// <summary> + /// The viewstate key to use for storing the value of the <see cref="ProviderEndpointUrl"/> property. + /// </summary> + private const string ProviderEndpointUrlViewStateKey = "ProviderEndpointUrl"; + + #endregion + + /// <summary> + /// The default value for the <see cref="ProviderVersion"/> property. + /// </summary> + private const ProtocolVersion ProviderVersionDefault = ProtocolVersion.V20; + + /// <summary> + /// Initializes a new instance of the <see cref="IdentityEndpoint"/> class. + /// </summary> + public IdentityEndpoint() { + } + + /// <summary> + /// Fired at each page request so the host web site can return the normalized + /// version of the request URI. + /// </summary> + public event EventHandler<IdentityEndpointNormalizationEventArgs> NormalizeUri; + + #region Properties + + /// <summary> + /// Gets or sets the OpenID version supported by the provider. + /// If multiple versions are supported, this should be set to the latest + /// version that this library and the Provider both support. + /// </summary> + [Category("Behavior")] + [DefaultValue(ProviderVersionDefault)] + [Description("The OpenID version supported by the provider.")] + public ProtocolVersion ProviderVersion { + get { + return this.ViewState[ProviderVersionViewStateKey] == null ? + ProviderVersionDefault : (ProtocolVersion)this.ViewState[ProviderVersionViewStateKey]; + } + + set { + this.ViewState[ProviderVersionViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets the Provider URL that processes OpenID requests. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Forms designer property grid only supports primitive types.")] + [Bindable(true), Category("Behavior")] + [Description("The Provider URL that processes OpenID requests.")] + [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] + public string ProviderEndpointUrl { + get { + return (string)ViewState[ProviderEndpointUrlViewStateKey]; + } + + set { + UriUtil.ValidateResolvableUrl(Page, DesignMode, value); + ViewState[ProviderEndpointUrlViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets the Identifier that is controlled by the Provider. + /// </summary> + [Bindable(true)] + [Category("Behavior")] + [Description("The user Identifier that is controlled by the Provider.")] + public string ProviderLocalIdentifier { + get { + return (string)ViewState[ProviderLocalIdentifierViewStateKey]; + } + + set { + UriUtil.ValidateResolvableUrl(Page, DesignMode, value); + ViewState[ProviderLocalIdentifierViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether every incoming request + /// will be checked for normalized form and redirected if it is not. + /// </summary> + /// <remarks> + /// <para>If set to true (and it should be), you should also handle the <see cref="NormalizeUri"/> + /// event and apply your own policy for normalizing the URI.</para> + /// If multiple <see cref="IdentityEndpoint"/> controls are on a single page (to support + /// multiple versions of OpenID for example) then only one of them should have this + /// property set to true. + /// </remarks> + [Bindable(true)] + [Category("Behavior")] + [Description("Whether every incoming request will be checked for normalized form and redirected if it is not. If set to true, consider handling the NormalizeUri event.")] + public bool AutoNormalizeRequest { + get { return (bool)(ViewState[AutoNormalizeRequestViewStateKey] ?? false); } + set { ViewState[AutoNormalizeRequestViewStateKey] = value; } + } + #endregion + + /// <summary> + /// Gets the protocol to use for advertising OpenID on the identity page. + /// </summary> + internal Protocol Protocol { + get { return Protocol.Lookup(this.ProviderVersion); } + } + + /// <summary> + /// Checks the incoming request and invokes a browser redirect if the URL has not been normalized. + /// </summary> + /// <seealso cref="IdentityEndpointNormalizationEventArgs.NormalizedIdentifier"/> + protected virtual void OnNormalize() { + UriIdentifier userSuppliedIdentifier = MessagingUtilities.GetRequestUrlFromContext(); + var normalizationArgs = new IdentityEndpointNormalizationEventArgs(userSuppliedIdentifier); + + var normalizeUri = this.NormalizeUri; + if (normalizeUri != null) { + normalizeUri(this, normalizationArgs); + } else { + // Do some best-guess normalization. + normalizationArgs.NormalizedIdentifier = BestGuessNormalization(normalizationArgs.UserSuppliedIdentifier); + } + + // If we have a normalized form, we should use it. + // We compare path and query with case sensitivity and host name without case sensitivity deliberately, + // and the fragment will be asserted or cleared by the OP during authentication. + if (normalizationArgs.NormalizedIdentifier != null && + (!String.Equals(normalizationArgs.NormalizedIdentifier.Host, normalizationArgs.UserSuppliedIdentifier.Host, StringComparison.OrdinalIgnoreCase) || + !String.Equals(normalizationArgs.NormalizedIdentifier.PathAndQuery, normalizationArgs.UserSuppliedIdentifier.PathAndQuery, StringComparison.Ordinal))) { + Page.Response.Redirect(normalizationArgs.NormalizedIdentifier.AbsoluteUri); + } + } + + /// <summary> + /// Checks the incoming request and invokes a browser redirect if the URL has not been normalized. + /// </summary> + /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param> + protected override void OnLoad(EventArgs e) { + // Perform URL normalization BEFORE calling base.OnLoad, to keep + // our base XrdsPublisher from over-eagerly responding with an XRDS + // document before we've redirected. + if (this.AutoNormalizeRequest && !this.Page.IsPostBack) { + this.OnNormalize(); + } + + base.OnLoad(e); + } + + /// <summary> + /// Renders OpenID identity tags. + /// </summary> + /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] + protected override void Render(HtmlTextWriter writer) { + Uri requestUrlBeforeRewrites = MessagingUtilities.GetRequestUrlFromContext(); + base.Render(writer); + if (!string.IsNullOrEmpty(this.ProviderEndpointUrl)) { + writer.WriteBeginTag("link"); + writer.WriteAttribute("rel", this.Protocol.HtmlDiscoveryProviderKey); + writer.WriteAttribute("href", new Uri(requestUrlBeforeRewrites, this.Page.ResolveUrl(this.ProviderEndpointUrl)).AbsoluteUri); + writer.Write(">"); + writer.WriteEndTag("link"); + writer.WriteLine(); + } + if (!string.IsNullOrEmpty(this.ProviderLocalIdentifier)) { + writer.WriteBeginTag("link"); + writer.WriteAttribute("rel", Protocol.HtmlDiscoveryLocalIdKey); + writer.WriteAttribute("href", new Uri(requestUrlBeforeRewrites, this.Page.ResolveUrl(this.ProviderLocalIdentifier)).AbsoluteUri); + writer.Write(">"); + writer.WriteEndTag("link"); + writer.WriteLine(); + } + } + + /// <summary> + /// Normalizes the URL by making the path and query lowercase, and trimming trailing slashes. + /// </summary> + /// <param name="uri">The URI to normalize.</param> + /// <returns>The normalized URI.</returns> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "FxCop is probably right, but we've been lowercasing host names for normalization elsewhere in the project for a long time now.")] + private static Uri BestGuessNormalization(Uri uri) { + UriBuilder uriBuilder = new UriBuilder(uri); + uriBuilder.Path = uriBuilder.Path.ToLowerInvariant(); + + // Ensure no trailing slash unless it is the only element of the path. + if (uriBuilder.Path != "/") { + uriBuilder.Path = uriBuilder.Path.TrimEnd('/'); + } + + // We trim the ? from the start of the query when we reset it because + // the UriBuilder.Query setter automatically prepends one, and we don't + // want to double them up. + uriBuilder.Query = uriBuilder.Query.TrimStart('?').ToLowerInvariant(); + return uriBuilder.Uri; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/IdentityEndpointNormalizationEventArgs.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IdentityEndpointNormalizationEventArgs.cs new file mode 100644 index 0000000..d190792 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/IdentityEndpointNormalizationEventArgs.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// <copyright file="IdentityEndpointNormalizationEventArgs.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + + /// <summary> + /// The event arguments passed to the <see cref="IdentityEndpoint.NormalizeUri"/> event handler. + /// </summary> + public class IdentityEndpointNormalizationEventArgs : EventArgs { + /// <summary> + /// Initializes a new instance of the <see cref="IdentityEndpointNormalizationEventArgs"/> class. + /// </summary> + /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> + internal IdentityEndpointNormalizationEventArgs(UriIdentifier userSuppliedIdentifier) { + this.UserSuppliedIdentifier = userSuppliedIdentifier; + } + + /// <summary> + /// Gets or sets the portion of the incoming page request URI that is relevant to normalization. + /// </summary> + /// <remarks> + /// This identifier should be used to look up the user whose identity page is being queried. + /// It MAY be set in case some clever web server URL rewriting is taking place that ASP.NET + /// does not know about but your site does. If this is the case this property should be set + /// to whatever the original request URL was. + /// </remarks> + public Uri UserSuppliedIdentifier { get; set; } + + /// <summary> + /// Gets or sets the normalized form of the user's identifier, according to the host site's policy. + /// </summary> + /// <remarks> + /// <para>This should be set to some constant value for an individual user. + /// For example, if <see cref="UserSuppliedIdentifier"/> indicates that identity page + /// for "BOB" is being called up, then the following things should be considered:</para> + /// <list> + /// <item>Normalize the capitalization of the URL: for example, change http://provider/BOB to + /// http://provider/bob.</item> + /// <item>Switch to HTTPS is it is offered: change http://provider/bob to https://provider/bob.</item> + /// <item>Strip off the query string if it is not part of the canonical identity: + /// https://provider/bob?timeofday=now becomes https://provider/bob</item> + /// <item>Ensure that any trailing slash is either present or absent consistently. For example, + /// change https://provider/bob/ to https://provider/bob.</item> + /// </list> + /// <para>When this property is set, the <see cref="IdentityEndpoint"/> control compares it to + /// the request that actually came in, and redirects the browser to use the normalized identifier + /// if necessary.</para> + /// <para>Using the normalized identifier in the request is <i>very</i> important as it + /// helps the user maintain a consistent identity across sites and across site visits to an individual site. + /// For example, without normalizing the URL, Bob might sign into a relying party site as + /// http://provider/bob one day and https://provider/bob the next day, and the relying party + /// site <i>should</i> interpret Bob as two different people because the URLs are different. + /// By normalizing the URL at the Provider's identity page for Bob, whichever URL Bob types in + /// from day-to-day gets redirected to a normalized form, so Bob is seen as the same person + /// all the time, which is of course what Bob wants. + /// </para> + /// </remarks> + public Uri NormalizedIdentifier { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs new file mode 100644 index 0000000..58dfc2f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs @@ -0,0 +1,662 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdProvider.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + 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.Linq; + using System.Threading; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + using RP = DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Offers services for a web page that is acting as an OpenID identity server. + /// </summary> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By design")] + [ContractVerification(true)] + public sealed class OpenIdProvider : IDisposable { + /// <summary> + /// The name of the key to use in the HttpApplication cache to store the + /// instance of <see cref="StandardProviderApplicationStore"/> to use. + /// </summary> + private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.Provider.OpenIdProvider.ApplicationStore"; + + /// <summary> + /// Backing store for the <see cref="Behaviors"/> property. + /// </summary> + private readonly ObservableCollection<IProviderBehavior> behaviors = new ObservableCollection<IProviderBehavior>(); + + /// <summary> + /// Backing field for the <see cref="SecuritySettings"/> property. + /// </summary> + private ProviderSecuritySettings securitySettings; + + /// <summary> + /// The relying party used to perform discovery on identifiers being sent in + /// unsolicited positive assertions. + /// </summary> + private RP.OpenIdRelyingParty relyingParty; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProvider"/> class. + /// </summary> + public OpenIdProvider() + : this(DotNetOpenAuthSection.Configuration.OpenId.Provider.ApplicationStore.CreateInstance(HttpApplicationStore)) { + Contract.Ensures(this.SecuritySettings != null); + Contract.Ensures(this.Channel != null); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProvider"/> class. + /// </summary> + /// <param name="applicationStore">The application store to use. Cannot be null.</param> + public OpenIdProvider(IOpenIdApplicationStore applicationStore) + : this((INonceStore)applicationStore, (ICryptoKeyStore)applicationStore) { + Contract.Requires<ArgumentNullException>(applicationStore != null); + Contract.Ensures(this.SecuritySettings != null); + Contract.Ensures(this.Channel != null); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProvider"/> class. + /// </summary> + /// <param name="nonceStore">The nonce store to use. Cannot be null.</param> + /// <param name="cryptoKeyStore">The crypto key store. Cannot be null.</param> + private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(nonceStore != null); + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Ensures(this.SecuritySettings != null); + Contract.Ensures(this.Channel != null); + + this.SecuritySettings = DotNetOpenAuthSection.Configuration.OpenId.Provider.SecuritySettings.CreateSecuritySettings(); + this.behaviors.CollectionChanged += this.OnBehaviorsChanged; + foreach (var behavior in DotNetOpenAuthSection.Configuration.OpenId.Provider.Behaviors.CreateInstances(false)) { + this.behaviors.Add(behavior); + } + + this.AssociationStore = new SwitchingAssociationStore(cryptoKeyStore, this.SecuritySettings); + this.Channel = new OpenIdChannel(this.AssociationStore, nonceStore, this.SecuritySettings); + this.CryptoKeyStore = cryptoKeyStore; + + Reporting.RecordFeatureAndDependencyUse(this, nonceStore); + } + + /// <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.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<IOpenIdApplicationStore>() != null); + HttpContext context = HttpContext.Current; + 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 StandardProviderApplicationStore(); + } + } finally { + context.Application.UnLock(); + } + } + + return store; + } + } + + /// <summary> + /// Gets the channel to use for sending/receiving messages. + /// </summary> + public Channel Channel { get; internal set; } + + /// <summary> + /// Gets the security settings used by this Provider. + /// </summary> + public ProviderSecuritySettings SecuritySettings { + get { + Contract.Ensures(Contract.Result<ProviderSecuritySettings>() != null); + Contract.Assume(this.securitySettings != null); + return this.securitySettings; + } + + internal set { + Contract.Requires<ArgumentNullException>(value != null); + this.securitySettings = value; + } + } + + /// <summary> + /// Gets the extension factories. + /// </summary> + public IList<IOpenIdExtensionFactory> ExtensionFactories { + get { return this.Channel.GetExtensionFactories(); } + } + + /// <summary> + /// Gets or sets the mechanism a host site can use to receive + /// notifications of errors when communicating with remote parties. + /// </summary> + public IErrorReporting ErrorReporting { get; set; } + + /// <summary> + /// Gets a list of custom behaviors to apply to OpenID actions. + /// </summary> + /// <remarks> + /// Adding behaviors can impact the security settings of the <see cref="OpenIdProvider"/> + /// in ways that subsequently removing the behaviors will not reverse. + /// </remarks> + public ICollection<IProviderBehavior> Behaviors { + get { return this.behaviors; } + } + + /// <summary> + /// Gets the crypto key store. + /// </summary> + public ICryptoKeyStore CryptoKeyStore { get; private set; } + + /// <summary> + /// Gets the association store. + /// </summary> + internal IProviderAssociationStore AssociationStore { get; private set; } + + /// <summary> + /// Gets the channel. + /// </summary> + internal OpenIdChannel OpenIdChannel { + get { return (OpenIdChannel)this.Channel; } + } + + /// <summary> + /// Gets the list of services that can perform discovery on identifiers given to this relying party. + /// </summary> + internal IList<IIdentifierDiscoveryService> DiscoveryServices { + get { return this.RelyingParty.DiscoveryServices; } + } + + /// <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 relying party used for discovery of identifiers sent in unsolicited assertions. + /// </summary> + private RP.OpenIdRelyingParty RelyingParty { + get { + if (this.relyingParty == null) { + lock (this) { + if (this.relyingParty == null) { + // we just need an RP that's capable of discovery, so stateless mode is fine. + this.relyingParty = new RP.OpenIdRelyingParty(null); + } + } + } + + this.relyingParty.Channel.WebRequestHandler = this.WebRequestHandler; + return this.relyingParty; + } + } + + /// <summary> + /// Gets the incoming OpenID request if there is one, or null if none was detected. + /// </summary> + /// <returns>The request that the hosting Provider should possibly process and then transmit the response for.</returns> + /// <remarks> + /// <para>Requests may be infrastructural to OpenID and allow auto-responses, or they may + /// be authentication requests where the Provider site has to make decisions based + /// on its own user database and policies.</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> + /// <exception cref="ProtocolException">Thrown if the incoming message is recognized but deviates from the protocol specification irrecoverably.</exception> + public IRequest GetRequest() { + return this.GetRequest(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Gets the incoming OpenID request if there is one, or null if none was detected. + /// </summary> + /// <param name="httpRequestInfo">The incoming HTTP request to extract the message from.</param> + /// <returns> + /// The request that the hosting Provider should process and then transmit the response for. + /// Null if no valid OpenID request was detected in the given HTTP request. + /// </returns> + /// <remarks> + /// Requests may be infrastructural to OpenID and allow auto-responses, or they may + /// be authentication requests where the Provider site has to make decisions based + /// on its own user database and policies. + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the incoming message is recognized + /// but deviates from the protocol specification irrecoverably.</exception> + public IRequest GetRequest(HttpRequestInfo httpRequestInfo) { + Contract.Requires<ArgumentNullException>(httpRequestInfo != null); + IDirectedProtocolMessage incomingMessage = null; + + try { + incomingMessage = this.Channel.ReadFromRequest(httpRequestInfo); + if (incomingMessage == null) { + // If the incoming request does not resemble an OpenID message at all, + // it's probably a user who just navigated to this URL, and we should + // just return null so the host can display a message to the user. + if (httpRequestInfo.HttpMethod == "GET" && !httpRequestInfo.UrlBeforeRewriting.QueryStringContainPrefixedParameters(Protocol.Default.openid.Prefix)) { + return null; + } + + ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); + } + + IRequest result = null; + + var checkIdMessage = incomingMessage as CheckIdRequest; + if (checkIdMessage != null) { + result = new AuthenticationRequest(this, checkIdMessage); + } + + if (result == null) { + var extensionOnlyRequest = incomingMessage as SignedResponseRequest; + if (extensionOnlyRequest != null) { + result = new AnonymousRequest(this, extensionOnlyRequest); + } + } + + if (result == null) { + var checkAuthMessage = incomingMessage as CheckAuthenticationRequest; + if (checkAuthMessage != null) { + result = new AutoResponsiveRequest(incomingMessage, new CheckAuthenticationResponse(checkAuthMessage, this), this.SecuritySettings); + } + } + + if (result == null) { + var associateMessage = incomingMessage as AssociateRequest; + if (associateMessage != null) { + result = new AutoResponsiveRequest(incomingMessage, associateMessage.CreateResponse(this.AssociationStore, this.SecuritySettings), this.SecuritySettings); + } + } + + if (result != null) { + foreach (var behavior in this.Behaviors) { + if (behavior.OnIncomingRequest(result)) { + // This behavior matched this request. + break; + } + } + + return result; + } + + throw ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); + } catch (ProtocolException ex) { + IRequest errorResponse = this.GetErrorResponse(ex, httpRequestInfo, incomingMessage); + if (errorResponse == null) { + throw; + } + + return errorResponse; + } + } + + /// <summary> + /// Sends the response to a received request. + /// </summary> + /// <param name="request">The incoming OpenID request whose response is to be sent.</param> + /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> + /// <remarks> + /// <para>Requires an HttpContext.Current context. If one is not available, the caller should use + /// <see cref="PrepareResponse"/> instead and manually send the <see cref="OutgoingWebResponse"/> + /// to the client.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use the Respond method instead, and prepare for execution to continue on this page beyond the call to Respond.")] + public void SendResponse(IRequest request) { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentException>(request.IsResponseReady); + + this.ApplyBehaviorsToResponse(request); + Request requestInternal = (Request)request; + this.Channel.Send(requestInternal.Response); + } + + /// <summary> + /// Sends the response to a received request. + /// </summary> + /// <param name="request">The incoming OpenID request whose response is to be sent.</param> + /// <remarks> + /// <para>Requires an HttpContext.Current context. If one is not available, the caller should use + /// <see cref="PrepareResponse"/> instead and manually send the <see cref="OutgoingWebResponse"/> + /// to the client.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] + public void Respond(IRequest request) { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentException>(request.IsResponseReady); + + this.ApplyBehaviorsToResponse(request); + Request requestInternal = (Request)request; + this.Channel.Respond(requestInternal.Response); + } + + /// <summary> + /// Gets the response to a received request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>The response that should be sent to the client.</returns> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] + public OutgoingWebResponse PrepareResponse(IRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentException>(request.IsResponseReady); + + this.ApplyBehaviorsToResponse(request); + Request requestInternal = (Request)request; + return this.Channel.PrepareResponse(requestInternal.Response); + } + + /// <summary> + /// Sends an identity assertion on behalf of one of this Provider's + /// members in order to redirect the user agent to a relying party + /// web site and log him/her in immediately in one uninterrupted step. + /// </summary> + /// <param name="providerEndpoint">The absolute URL on the Provider site that receives OpenID messages.</param> + /// <param name="relyingPartyRealm">The URL of the Relying Party web site. + /// This will typically be the home page, but may be a longer URL if + /// that Relying Party considers the scope of its realm to be more specific. + /// The URL provided here must allow discovery of the Relying Party's + /// XRDS document that advertises its OpenID RP endpoint.</param> + /// <param name="claimedIdentifier">The Identifier you are asserting your member controls.</param> + /// <param name="localIdentifier">The Identifier you know your user by internally. This will typically + /// be the same as <paramref name="claimedIdentifier"/>.</param> + /// <param name="extensions">The extensions.</param> + public void SendUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<ArgumentNullException>(providerEndpoint != null); + Contract.Requires<ArgumentException>(providerEndpoint.IsAbsoluteUri); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); + Contract.Requires<ArgumentNullException>(claimedIdentifier != null); + Contract.Requires<ArgumentNullException>(localIdentifier != null); + + this.PrepareUnsolicitedAssertion(providerEndpoint, relyingPartyRealm, claimedIdentifier, localIdentifier, extensions).Respond(); + } + + /// <summary> + /// Prepares an identity assertion on behalf of one of this Provider's + /// members in order to redirect the user agent to a relying party + /// web site and log him/her in immediately in one uninterrupted step. + /// </summary> + /// <param name="providerEndpoint">The absolute URL on the Provider site that receives OpenID messages.</param> + /// <param name="relyingPartyRealm">The URL of the Relying Party web site. + /// This will typically be the home page, but may be a longer URL if + /// that Relying Party considers the scope of its realm to be more specific. + /// The URL provided here must allow discovery of the Relying Party's + /// XRDS document that advertises its OpenID RP endpoint.</param> + /// <param name="claimedIdentifier">The Identifier you are asserting your member controls.</param> + /// <param name="localIdentifier">The Identifier you know your user by internally. This will typically + /// be the same as <paramref name="claimedIdentifier"/>.</param> + /// <param name="extensions">The extensions.</param> + /// <returns> + /// A <see cref="OutgoingWebResponse"/> object describing the HTTP response to send + /// the user agent to allow the redirect with assertion to happen. + /// </returns> + public OutgoingWebResponse PrepareUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { + Contract.Requires<ArgumentNullException>(providerEndpoint != null); + Contract.Requires<ArgumentException>(providerEndpoint.IsAbsoluteUri); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); + Contract.Requires<ArgumentNullException>(claimedIdentifier != null); + Contract.Requires<ArgumentNullException>(localIdentifier != null); + Contract.Requires<InvalidOperationException>(this.Channel.WebRequestHandler != null); + + // Although the RP should do their due diligence to make sure that this OP + // is authorized to send an assertion for the given claimed identifier, + // do due diligence by performing our own discovery on the claimed identifier + // and make sure that it is tied to this OP and OP local identifier. + if (this.SecuritySettings.UnsolicitedAssertionVerification != ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.NeverVerify) { + var serviceEndpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, localIdentifier, new ProviderEndpointDescription(providerEndpoint, Protocol.Default.Version), null, null); + var discoveredEndpoints = this.RelyingParty.Discover(claimedIdentifier); + if (!discoveredEndpoints.Contains(serviceEndpoint)) { + Logger.OpenId.WarnFormat( + "Failed to send unsolicited assertion for {0} because its discovered services did not include this endpoint: {1}{2}{1}Discovered endpoints: {1}{3}", + claimedIdentifier, + Environment.NewLine, + serviceEndpoint, + discoveredEndpoints.ToStringDeferred(true)); + + // Only FAIL if the setting is set for it. + if (this.securitySettings.UnsolicitedAssertionVerification == ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.RequireSuccess) { + ErrorUtilities.ThrowProtocol(OpenIdStrings.UnsolicitedAssertionForUnrelatedClaimedIdentifier, claimedIdentifier); + } + } + } + + Logger.OpenId.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier); + RelyingPartyEndpointDescription returnToEndpoint = null; + var returnToEndpoints = relyingPartyRealm.DiscoverReturnToEndpoints(this.WebRequestHandler, true); + if (returnToEndpoints != null) { + returnToEndpoint = returnToEndpoints.FirstOrDefault(); + } + ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingPartyRealm); + + var positiveAssertion = new PositiveAssertionResponse(returnToEndpoint) { + ProviderEndpoint = providerEndpoint, + ClaimedIdentifier = claimedIdentifier, + LocalIdentifier = localIdentifier, + }; + + if (extensions != null) { + foreach (IExtensionMessage extension in extensions) { + positiveAssertion.Extensions.Add(extension); + } + } + + Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion"); + return this.Channel.PrepareResponse(positiveAssertion); + } + + #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); + } + + /// <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> + private void Dispose(bool disposing) { + if (disposing) { + // Tear off the instance member as a local variable for thread safety. + IDisposable channel = this.Channel as IDisposable; + if (channel != null) { + channel.Dispose(); + } + + if (this.relyingParty != null) { + this.relyingParty.Dispose(); + } + } + } + + #endregion + + /// <summary> + /// Applies all behaviors to the response message. + /// </summary> + /// <param name="request">The request.</param> + private void ApplyBehaviorsToResponse(IRequest request) { + var authRequest = request as IAuthenticationRequest; + if (authRequest != null) { + foreach (var behavior in this.Behaviors) { + if (behavior.OnOutgoingResponse(authRequest)) { + // This behavior matched this request. + break; + } + } + } + } + + /// <summary> + /// Prepares the return value for the GetRequest method in the event of an exception. + /// </summary> + /// <param name="ex">The exception that forms the basis of the error response. Must not be null.</param> + /// <param name="httpRequestInfo">The incoming HTTP request. Must not be null.</param> + /// <param name="incomingMessage">The incoming message. May be null in the case that it was malformed.</param> + /// <returns> + /// Either the <see cref="IRequest"/> to return to the host site or null to indicate no response could be reasonably created and that the caller should rethrow the exception. + /// </returns> + private IRequest GetErrorResponse(ProtocolException ex, HttpRequestInfo httpRequestInfo, IDirectedProtocolMessage incomingMessage) { + Contract.Requires<ArgumentNullException>(ex != null); + Contract.Requires<ArgumentNullException>(httpRequestInfo != null); + + Logger.OpenId.Error("An exception was generated while processing an incoming OpenID request.", ex); + IErrorMessage errorMessage; + + // We must create the appropriate error message type (direct vs. indirect) + // based on what we see in the request. + string returnTo = httpRequestInfo.QueryString[Protocol.Default.openid.return_to]; + if (returnTo != null) { + // An indirect request message from the RP + // We need to return an indirect response error message so the RP can consume it. + // Consistent with OpenID 2.0 section 5.2.3. + var indirectRequest = incomingMessage as SignedResponseRequest; + if (indirectRequest != null) { + errorMessage = new IndirectErrorResponse(indirectRequest); + } else { + errorMessage = new IndirectErrorResponse(Protocol.Default.Version, new Uri(returnTo)); + } + } else if (httpRequestInfo.HttpMethod == "POST") { + // A direct request message from the RP + // We need to return a direct response error message so the RP can consume it. + // Consistent with OpenID 2.0 section 5.1.2.2. + errorMessage = new DirectErrorResponse(Protocol.Default.Version, incomingMessage); + } else { + // This may be an indirect request from an RP that was so badly + // formed that we cannot even return an error to the RP. + // The best we can do is display an error to the user. + // Returning null cues the caller to "throw;" + return null; + } + + errorMessage.ErrorMessage = ex.ToStringDescriptive(); + + // Allow host to log this error and issue a ticket #. + // We tear off the field to a local var for thread safety. + IErrorReporting hostErrorHandler = this.ErrorReporting; + if (hostErrorHandler != null) { + errorMessage.Contact = hostErrorHandler.Contact; + errorMessage.Reference = hostErrorHandler.LogError(ex); + } + + if (incomingMessage != null) { + return new AutoResponsiveRequest(incomingMessage, errorMessage, this.SecuritySettings); + } else { + return new AutoResponsiveRequest(errorMessage, this.SecuritySettings); + } + } + + /// <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 (IProviderBehavior profile in e.NewItems) { + profile.ApplySecuritySettings(this.SecuritySettings); + Reporting.RecordFeatureUse(profile); + } + } + + /// <summary> + /// Provides a single OP association store instance that can handle switching between + /// association handle encoding modes. + /// </summary> + private class SwitchingAssociationStore : IProviderAssociationStore { + /// <summary> + /// The security settings of the Provider. + /// </summary> + private readonly ProviderSecuritySettings securitySettings; + + /// <summary> + /// The association store that records association secrets in the association handles themselves. + /// </summary> + private IProviderAssociationStore associationHandleEncoder; + + /// <summary> + /// The association store that records association secrets in a secret store. + /// </summary> + private IProviderAssociationStore associationSecretStorage; + + /// <summary> + /// Initializes a new instance of the <see cref="SwitchingAssociationStore"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + /// <param name="securitySettings">The security settings.</param> + internal SwitchingAssociationStore(ICryptoKeyStore cryptoKeyStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + this.securitySettings = securitySettings; + + this.associationHandleEncoder = new ProviderAssociationHandleEncoder(cryptoKeyStore); + this.associationSecretStorage = new ProviderAssociationKeyStorage(cryptoKeyStore); + } + + /// <summary> + /// Gets the association store that applies given the Provider's current security settings. + /// </summary> + internal IProviderAssociationStore AssociationStore { + get { return this.securitySettings.EncodeAssociationSecretsInHandles ? this.associationHandleEncoder : this.associationSecretStorage; } + } + + /// <summary> + /// Stores an association and returns a handle for it. + /// </summary> + /// <param name="secret">The association secret.</param> + /// <param name="expiresUtc">The UTC time that the association should expire.</param> + /// <param name="privateAssociation">A value indicating whether this is a private association.</param> + /// <returns> + /// The association handle that represents this association. + /// </returns> + public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) { + return this.AssociationStore.Serialize(secret, expiresUtc, privateAssociation); + } + + /// <summary> + /// Retrieves an association given an association handle. + /// </summary> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). + /// </returns> + /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception> + public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) { + return this.AssociationStore.Deserialize(containingMessage, isPrivateAssociation, handle); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs new file mode 100644 index 0000000..46e172c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/PrivatePersonalIdentifierProviderBase.cs @@ -0,0 +1,224 @@ +//----------------------------------------------------------------------- +// <copyright file="PrivatePersonalIdentifierProviderBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Provides standard PPID Identifiers to users to protect their identity from individual relying parties + /// and from colluding groups of relying parties. + /// </summary> + public abstract class PrivatePersonalIdentifierProviderBase : IDirectedIdentityIdentifierProvider { + /// <summary> + /// The type of hash function to use for the <see cref="Hasher"/> property. + /// </summary> + private const string HashAlgorithmName = "SHA256"; + + /// <summary> + /// The length of the salt to generate for first time PPID-users. + /// </summary> + private int newSaltLength = 20; + + /// <summary> + /// Initializes a new instance of the <see cref="PrivatePersonalIdentifierProviderBase"/> class. + /// </summary> + /// <param name="baseIdentifier">The base URI on which to append the anonymous part.</param> + protected PrivatePersonalIdentifierProviderBase(Uri baseIdentifier) { + Contract.Requires<ArgumentNullException>(baseIdentifier != null); + + this.Hasher = HashAlgorithm.Create(HashAlgorithmName); + this.Encoder = Encoding.UTF8; + this.BaseIdentifier = baseIdentifier; + this.PairwiseUnique = AudienceScope.Realm; + } + + /// <summary> + /// A granularity description for who wide of an audience sees the same generated PPID. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "Breaking change")] + public enum AudienceScope { + /// <summary> + /// A unique Identifier is generated for every realm. This is the highest security setting. + /// </summary> + Realm, + + /// <summary> + /// Only the host name in the realm is used in calculating the PPID, + /// allowing for some level of sharing of the PPID Identifiers between RPs + /// that are able to share the same realm host value. + /// </summary> + RealmHost, + + /// <summary> + /// Although the user's Identifier is still opaque to the RP so they cannot determine + /// who the user is at the OP, the same Identifier is used at all RPs so collusion + /// between the RPs is possible. + /// </summary> + Global, + } + + /// <summary> + /// Gets the base URI on which to append the anonymous part. + /// </summary> + public Uri BaseIdentifier { get; private set; } + + /// <summary> + /// Gets or sets a value indicating whether each Realm will get its own private identifier + /// for the authenticating uesr. + /// </summary> + /// <value>The default value is <see cref="AudienceScope.Realm"/>.</value> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Pairwise", Justification = "Meaningful word")] + public AudienceScope PairwiseUnique { get; set; } + + /// <summary> + /// Gets the hash function to use to perform the one-way transform of a personal identifier + /// to an "anonymous" looking one. + /// </summary> + protected HashAlgorithm Hasher { get; private set; } + + /// <summary> + /// Gets the encoder to use for transforming the personal identifier into bytes for hashing. + /// </summary> + protected Encoding Encoder { get; private set; } + + /// <summary> + /// Gets or sets the new length of the salt. + /// </summary> + /// <value>The new length of the salt.</value> + protected int NewSaltLength { + get { + return this.newSaltLength; + } + + set { + Contract.Requires<ArgumentOutOfRangeException>(value > 0); + this.newSaltLength = value; + } + } + + #region IDirectedIdentityIdentifierProvider Members + + /// <summary> + /// Gets the Identifier to use for the Claimed Identifier and Local Identifier of + /// an outgoing positive assertion. + /// </summary> + /// <param name="localIdentifier">The OP local identifier for the authenticating user.</param> + /// <param name="relyingPartyRealm">The realm of the relying party receiving the assertion.</param> + /// <returns> + /// A valid, discoverable OpenID Identifier that should be used as the value for the + /// openid.claimed_id and openid.local_id parameters. Must not be null. + /// </returns> + public Uri GetIdentifier(Identifier localIdentifier, Realm relyingPartyRealm) { + byte[] salt = this.GetHashSaltForLocalIdentifier(localIdentifier); + string valueToHash = localIdentifier + "#"; + switch (this.PairwiseUnique) { + case AudienceScope.Realm: + valueToHash += relyingPartyRealm; + break; + case AudienceScope.RealmHost: + valueToHash += relyingPartyRealm.Host; + break; + case AudienceScope.Global: + break; + default: + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + OpenIdStrings.UnexpectedEnumPropertyValue, + "PairwiseUnique", + this.PairwiseUnique)); + } + + byte[] valueAsBytes = this.Encoder.GetBytes(valueToHash); + byte[] bytesToHash = new byte[valueAsBytes.Length + salt.Length]; + valueAsBytes.CopyTo(bytesToHash, 0); + salt.CopyTo(bytesToHash, valueAsBytes.Length); + byte[] hash = this.Hasher.ComputeHash(bytesToHash); + string base64Hash = Convert.ToBase64String(hash); + Uri anonymousIdentifier = this.AppendIdentifiers(base64Hash); + return anonymousIdentifier; + } + + /// <summary> + /// Determines whether a given identifier is the primary (non-PPID) local identifier for some user. + /// </summary> + /// <param name="identifier">The identifier in question.</param> + /// <returns> + /// <c>true</c> if the given identifier is the valid, unique identifier for some uesr (and NOT a PPID); otherwise, <c>false</c>. + /// </returns> + public virtual bool IsUserLocalIdentifier(Identifier identifier) + { + return !identifier.ToString().StartsWith(this.BaseIdentifier.AbsoluteUri, StringComparison.Ordinal); + } + + #endregion + + /// <summary> + /// Creates a new salt to assign to a user. + /// </summary> + /// <returns>A non-null buffer of length <see cref="NewSaltLength"/> filled with a random salt.</returns> + protected virtual byte[] CreateSalt() { + // We COULD use a crypto random function, but for a salt it seems overkill. + return MessagingUtilities.GetNonCryptoRandomData(this.NewSaltLength); + } + + /// <summary> + /// Creates a new PPID Identifier by appending a pseudonymous identifier suffix to + /// the <see cref="BaseIdentifier"/>. + /// </summary> + /// <param name="uriHash">The unique part of the Identifier to append to the common first part.</param> + /// <returns>The full PPID Identifier.</returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "NOT equivalent overload. The recommended one breaks on relative URIs.")] + protected virtual Uri AppendIdentifiers(string uriHash) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(uriHash)); + + if (string.IsNullOrEmpty(this.BaseIdentifier.Query)) { + // The uriHash will appear on the path itself. + string pathEncoded = Uri.EscapeUriString(uriHash.Replace('/', '_')); + return new Uri(this.BaseIdentifier, pathEncoded); + } else { + // The uriHash will appear on the query string. + string dataEncoded = Uri.EscapeDataString(uriHash); + return new Uri(this.BaseIdentifier + dataEncoded); + } + } + + /// <summary> + /// Gets the salt to use for generating an anonymous identifier for a given OP local identifier. + /// </summary> + /// <param name="localIdentifier">The OP local identifier.</param> + /// <returns>The salt to use in the hash.</returns> + /// <remarks> + /// It is important that this method always return the same value for a given + /// <paramref name="localIdentifier"/>. + /// New salts can be generated for local identifiers without previously assigned salt + /// values by calling <see cref="CreateSalt"/> or by a custom method. + /// </remarks> + protected abstract byte[] GetHashSaltForLocalIdentifier(Identifier localIdentifier); + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(this.Hasher != null); + Contract.Invariant(this.Encoder != null); + Contract.Invariant(this.BaseIdentifier != null); + Contract.Invariant(this.NewSaltLength > 0); + } +#endif + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderAssociationHandleEncoder.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderAssociationHandleEncoder.cs new file mode 100644 index 0000000..5de560c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderAssociationHandleEncoder.cs @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------- +// <copyright file="ProviderAssociationHandleEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + using System.Threading; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// Provides association storage in the association handle itself, but embedding signed and encrypted association + /// details in the handle. + /// </summary> + public class ProviderAssociationHandleEncoder : IProviderAssociationStore { + /// <summary> + /// The name of the bucket in which to store keys that encrypt association data into association handles. + /// </summary> + internal const string AssociationHandleEncodingSecretBucket = "https://localhost/dnoa/association_handles"; + + /// <summary> + /// The crypto key store used to persist encryption keys. + /// </summary> + private readonly ICryptoKeyStore cryptoKeyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="ProviderAssociationHandleEncoder"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + public ProviderAssociationHandleEncoder(ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + this.cryptoKeyStore = cryptoKeyStore; + } + + /// <summary> + /// Encodes the specified association data bag. + /// </summary> + /// <param name="secret">The symmetric secret.</param> + /// <param name="expiresUtc">The UTC time that the association should expire.</param> + /// <param name="privateAssociation">A value indicating whether this is a private association.</param> + /// <returns> + /// The association handle that represents this association. + /// </returns> + public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) { + var associationDataBag = new AssociationDataBag { + Secret = secret, + IsPrivateAssociation = privateAssociation, + ExpiresUtc = expiresUtc, + }; + + var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket, expiresUtc - DateTime.UtcNow); + return formatter.Serialize(associationDataBag); + } + + /// <summary> + /// Retrieves an association given an association handle. + /// </summary> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="privateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). + /// </returns> + /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception> + public Association Deserialize(IProtocolMessage containingMessage, bool privateAssociation, string handle) { + var formatter = AssociationDataBag.CreateFormatter(this.cryptoKeyStore, AssociationHandleEncodingSecretBucket); + AssociationDataBag bag; + try { + bag = formatter.Deserialize(containingMessage, handle); + } catch (ProtocolException ex) { + Logger.OpenId.Error("Rejecting an association because deserialization of the encoded handle failed.", ex); + return null; + } + + ErrorUtilities.VerifyProtocol(bag.IsPrivateAssociation == privateAssociation, "Unexpected association type."); + Association assoc = Association.Deserialize(handle, bag.ExpiresUtc, bag.Secret); + return assoc.IsExpired ? null : assoc; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderAssociationKeyStorage.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderAssociationKeyStorage.cs new file mode 100644 index 0000000..9801aac --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderAssociationKeyStorage.cs @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------- +// <copyright file="ProviderAssociationKeyStorage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An association storage mechanism that stores the association secrets in a private store, + /// and returns randomly generated association handles to refer to these secrets. + /// </summary> + internal class ProviderAssociationKeyStorage : IProviderAssociationStore { + /// <summary> + /// The bucket to use when recording shared associations. + /// </summary> + internal const string SharedAssociationBucket = "https://localhost/dnoa/shared_associations"; + + /// <summary> + /// The bucket to use when recording private associations. + /// </summary> + internal const string PrivateAssociationBucket = "https://localhost/dnoa/private_associations"; + + /// <summary> + /// The backing crypto key store. + /// </summary> + private readonly ICryptoKeyStore cryptoKeyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="ProviderAssociationKeyStorage"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The store where association secrets will be recorded.</param> + internal ProviderAssociationKeyStorage(ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + this.cryptoKeyStore = cryptoKeyStore; + } + + /// <summary> + /// Stores an association and returns a handle for it. + /// </summary> + /// <param name="secret">The association secret.</param> + /// <param name="expiresUtc">The UTC time that the association should expire.</param> + /// <param name="privateAssociation">A value indicating whether this is a private association.</param> + /// <returns> + /// The association handle that represents this association. + /// </returns> + public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) { + string handle; + this.cryptoKeyStore.StoreKey( + privateAssociation ? PrivateAssociationBucket : SharedAssociationBucket, + handle = OpenIdUtilities.GenerateRandomAssociationHandle(), + new CryptoKey(secret, expiresUtc)); + return handle; + } + + /// <summary> + /// Retrieves an association given an association handle. + /// </summary> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). + /// </returns> + /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception> + public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) { + var key = this.cryptoKeyStore.GetKey(isPrivateAssociation ? PrivateAssociationBucket : SharedAssociationBucket, handle); + if (key != null) { + return Association.Deserialize(handle, key.ExpiresUtc, key.Key); + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderEndpoint.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderEndpoint.cs new file mode 100644 index 0000000..821d95c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderEndpoint.cs @@ -0,0 +1,267 @@ +//----------------------------------------------------------------------- +// <copyright file="ProviderEndpoint.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.Contracts; + using System.Text; + using System.Web; + using System.Web.UI; + using System.Web.UI.WebControls; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// An OpenID Provider control that automatically responds to certain + /// automated OpenID messages, and routes authentication requests to + /// custom code via an event handler. + /// </summary> + [DefaultEvent("AuthenticationChallenge")] + [ToolboxData("<{0}:ProviderEndpoint runat='server' />")] + public class ProviderEndpoint : Control { + /// <summary> + /// The key used to store the pending authentication request in the ASP.NET session. + /// </summary> + private const string PendingRequestKey = "pendingRequest"; + + /// <summary> + /// The default value for the <see cref="Enabled"/> property. + /// </summary> + private const bool EnabledDefault = true; + + /// <summary> + /// The view state key in which to store the value of the <see cref="Enabled"/> property. + /// </summary> + private const string EnabledViewStateKey = "Enabled"; + + /// <summary> + /// Backing field for the <see cref="Provider"/> property. + /// </summary> + private static OpenIdProvider provider; + + /// <summary> + /// The lock that must be obtained when initializing the provider field. + /// </summary> + private static object providerInitializerLock = new object(); + + /// <summary> + /// Fired when an incoming OpenID request is an authentication challenge + /// that must be responded to by the Provider web site according to its + /// own user database and policies. + /// </summary> + public event EventHandler<AuthenticationChallengeEventArgs> AuthenticationChallenge; + + /// <summary> + /// Fired when an incoming OpenID message carries extension requests + /// but is not regarding any OpenID identifier. + /// </summary> + public event EventHandler<AnonymousRequestEventArgs> AnonymousRequest; + + /// <summary> + /// Gets or sets the <see cref="OpenIdProvider"/> instance to use for all instances of this control. + /// </summary> + /// <value>The default value is an <see cref="OpenIdProvider"/> instance initialized according to the web.config file.</value> + public static OpenIdProvider Provider { + get { + Contract.Ensures(Contract.Result<OpenIdProvider>() != null); + if (provider == null) { + lock (providerInitializerLock) { + if (provider == null) { + provider = CreateProvider(); + } + } + } + + return provider; + } + + set { + Contract.Requires<ArgumentNullException>(value != null); + provider = value; + } + } + + /// <summary> + /// Gets or sets an incoming OpenID authentication request that has not yet been responded to. + /// </summary> + /// <remarks> + /// This request is stored in the ASP.NET Session state, so it will survive across + /// redirects, postbacks, and transfers. This allows you to authenticate the user + /// yourself, and confirm his/her desire to authenticate to the relying party site + /// before responding to the relying party's authentication request. + /// </remarks> + public static IAuthenticationRequest PendingAuthenticationRequest { + get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + Contract.Ensures(Contract.Result<IAuthenticationRequest>() == null || PendingRequest != null); + return HttpContext.Current.Session[PendingRequestKey] as IAuthenticationRequest; + } + + set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + HttpContext.Current.Session[PendingRequestKey] = value; + } + } + + /// <summary> + /// Gets or sets an incoming OpenID anonymous request that has not yet been responded to. + /// </summary> + /// <remarks> + /// This request is stored in the ASP.NET Session state, so it will survive across + /// redirects, postbacks, and transfers. This allows you to authenticate the user + /// yourself, and confirm his/her desire to provide data to the relying party site + /// before responding to the relying party's request. + /// </remarks> + public static IAnonymousRequest PendingAnonymousRequest { + get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + Contract.Ensures(Contract.Result<IAnonymousRequest>() == null || PendingRequest != null); + return HttpContext.Current.Session[PendingRequestKey] as IAnonymousRequest; + } + + set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + HttpContext.Current.Session[PendingRequestKey] = value; + } + } + + /// <summary> + /// Gets or sets an incoming OpenID request that has not yet been responded to. + /// </summary> + /// <remarks> + /// This request is stored in the ASP.NET Session state, so it will survive across + /// redirects, postbacks, and transfers. This allows you to authenticate the user + /// yourself, and confirm his/her desire to provide data to the relying party site + /// before responding to the relying party's request. + /// </remarks> + public static IHostProcessedRequest PendingRequest { + get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest; + } + + set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + HttpContext.Current.Session[PendingRequestKey] = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether or not this control should + /// be listening for and responding to incoming OpenID requests. + /// </summary> + [Category("Behavior"), DefaultValue(EnabledDefault)] + public bool Enabled { + get { + return ViewState[EnabledViewStateKey] == null ? + EnabledDefault : (bool)ViewState[EnabledViewStateKey]; + } + + set { + ViewState[EnabledViewStateKey] = value; + } + } + + /// <summary> + /// Sends the response for the <see cref="PendingAuthenticationRequest"/> and clears the property. + /// </summary> + public static void SendResponse() { + var pendingRequest = PendingRequest; + PendingRequest = null; + Provider.Respond(pendingRequest); + } + + /// <summary> + /// Checks for incoming OpenID requests, responds to ones it can + /// respond to without policy checks, and fires events for custom + /// handling of the ones it cannot decide on automatically. + /// </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); + + // There is the unusual scenario that this control is hosted by + // an ASP.NET web page that has other UI on it to that the user + // might see, including controls that cause a postback to occur. + // We definitely want to ignore postbacks, since any openid messages + // they contain will be old. + if (this.Enabled && !this.Page.IsPostBack) { + // Use the explicitly given state store 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 OpenIdProvider. + // determine what incoming message was received + IRequest request = Provider.GetRequest(); + if (request != null) { + PendingRequest = null; + + // process the incoming message appropriately and send the response + IAuthenticationRequest idrequest; + IAnonymousRequest anonRequest; + if ((idrequest = request as IAuthenticationRequest) != null) { + PendingAuthenticationRequest = idrequest; + this.OnAuthenticationChallenge(idrequest); + } else if ((anonRequest = request as IAnonymousRequest) != null) { + PendingAnonymousRequest = anonRequest; + if (!this.OnAnonymousRequest(anonRequest)) { + // This is a feature not supported by the OP, so + // go ahead and set disapproved so we can send a response. + Logger.OpenId.Warn("An incoming anonymous OpenID request message was detected, but the ProviderEndpoint.AnonymousRequest event is not handled, so returning cancellation message to relying party."); + anonRequest.IsApproved = false; + } + } + if (request.IsResponseReady) { + Provider.Respond(request); + PendingAuthenticationRequest = null; + } + } + } + } + + /// <summary> + /// Fires the <see cref="AuthenticationChallenge"/> event. + /// </summary> + /// <param name="request">The request to include in the event args.</param> + protected virtual void OnAuthenticationChallenge(IAuthenticationRequest request) { + var authenticationChallenge = this.AuthenticationChallenge; + if (authenticationChallenge != null) { + authenticationChallenge(this, new AuthenticationChallengeEventArgs(request)); + } + } + + /// <summary> + /// Fires the <see cref="AnonymousRequest"/> event. + /// </summary> + /// <param name="request">The request to include in the event args.</param> + /// <returns><c>true</c> if there were any anonymous request handlers.</returns> + protected virtual bool OnAnonymousRequest(IAnonymousRequest request) { + var anonymousRequest = this.AnonymousRequest; + if (anonymousRequest != null) { + anonymousRequest(this, new AnonymousRequestEventArgs(request)); + return true; + } else { + return false; + } + } + + /// <summary> + /// Creates the default OpenIdProvider to use. + /// </summary> + /// <returns>The new instance of OpenIdProvider.</returns> + private static OpenIdProvider CreateProvider() { + Contract.Ensures(Contract.Result<OpenIdProvider>() != null); + return new OpenIdProvider(DotNetOpenAuthSection.Configuration.OpenId.Provider.ApplicationStore.CreateInstance(OpenIdProvider.HttpApplicationStore)); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderSecuritySettings.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderSecuritySettings.cs new file mode 100644 index 0000000..130e6dd --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/ProviderSecuritySettings.cs @@ -0,0 +1,167 @@ +//----------------------------------------------------------------------- +// <copyright file="ProviderSecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Security settings that are applicable to providers. + /// </summary> + [Serializable] + public sealed class ProviderSecuritySettings : SecuritySettings { + /// <summary> + /// The default value for the <see cref="ProtectDownlevelReplayAttacks"/> property. + /// </summary> + internal const bool ProtectDownlevelReplayAttacksDefault = true; + + /// <summary> + /// The default value for the <see cref="EncodeAssociationSecretsInHandles"/> property. + /// </summary> + internal const bool EncodeAssociationSecretsInHandlesDefault = true; + + /// <summary> + /// The default value for the <see cref="SignOutgoingExtensions"/> property. + /// </summary> + internal const bool SignOutgoingExtensionsDefault = true; + + /// <summary> + /// The default value for the <see cref="UnsolicitedAssertionVerification"/> property. + /// </summary> + internal const UnsolicitedAssertionVerificationLevel UnsolicitedAssertionVerificationDefault = UnsolicitedAssertionVerificationLevel.RequireSuccess; + + /// <summary> + /// The subset of association types and their customized lifetimes. + /// </summary> + private IDictionary<string, TimeSpan> associationLifetimes = new Dictionary<string, TimeSpan>(); + + /// <summary> + /// Initializes a new instance of the <see cref="ProviderSecuritySettings"/> class. + /// </summary> + internal ProviderSecuritySettings() + : base(true) { + this.SignOutgoingExtensions = SignOutgoingExtensionsDefault; + this.ProtectDownlevelReplayAttacks = ProtectDownlevelReplayAttacksDefault; + this.UnsolicitedAssertionVerification = UnsolicitedAssertionVerificationDefault; + } + + /// <summary> + /// The behavior a Provider takes when verifying that it is authoritative for an + /// identifier it is about to send an unsolicited assertion for. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "By design")] + public enum UnsolicitedAssertionVerificationLevel { + /// <summary> + /// Always verify that the Provider is authoritative for an identifier before + /// sending an unsolicited assertion for it and fail if it is not. + /// </summary> + RequireSuccess, + + /// <summary> + /// Always check that the Provider is authoritative for an identifier before + /// sending an unsolicited assertion for it, but only log failures, and proceed + /// to send the unsolicited assertion. + /// </summary> + LogWarningOnFailure, + + /// <summary> + /// Never verify that the Provider is authoritative for an identifier before + /// sending an unsolicited assertion for it. + /// </summary> + /// <remarks> + /// This setting is useful for web servers that refuse to allow a Provider to + /// introspectively perform an HTTP GET on itself, when sending unsolicited assertions + /// for identifiers that the OP controls. + /// </remarks> + NeverVerify, + } + + /// <summary> + /// Gets a subset of the available association types and their + /// customized maximum lifetimes. + /// </summary> + public IDictionary<string, TimeSpan> AssociationLifetimes { + get { return this.associationLifetimes; } + } + + /// <summary> + /// Gets or sets a value indicating whether Relying Party discovery will only + /// succeed if done over a secure HTTPS channel. + /// </summary> + /// <value>Default is <c>false</c>.</value> + public bool RequireSsl { get; set; } + + /// <summary> + /// Gets or sets the level of verification a Provider performs on an identifier before + /// sending an unsolicited assertion for it. + /// </summary> + /// <value>The default value is <see cref="UnsolicitedAssertionVerificationLevel.RequireSuccess"/>.</value> + public UnsolicitedAssertionVerificationLevel UnsolicitedAssertionVerification { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the Provider should ease the burden of storing associations + /// by encoding them in signed, encrypted form into the association handles themselves, storing only + /// a few rotating, private symmetric keys in the Provider's store instead. + /// </summary> + /// <value>The default value for this property is <c>true</c>.</value> + public bool EncodeAssociationSecretsInHandles { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether OpenID 1.x relying parties that may not be + /// protecting their users from replay attacks are protected from + /// replay attacks by this provider. + /// </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 preventing associations from being used + /// with OpenID 1.x relying parties, thereby forcing them into + /// "dumb" mode and verifying every claim with this provider. + /// This gives the provider an opportunity to verify its own nonce + /// to protect against replay attacks.</para> + /// </remarks> + internal bool ProtectDownlevelReplayAttacks { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether outgoing extensions are always signed. + /// </summary> + /// <value> + /// <c>true</c> if outgoing extensions should be signed; otherwise, <c>false</c>. + /// The default is <c>true</c>. + /// </value> + /// <remarks> + /// This property is internal because Providers should never turn it off, but it is + /// needed for testing the RP's rejection of unsigned extensions. + /// </remarks> + internal bool SignOutgoingExtensions { get; set; } + + /// <summary> + /// Creates a deep clone of this instance. + /// </summary> + /// <returns>A new instance that is a deep clone of this instance.</returns> + internal ProviderSecuritySettings Clone() { + var securitySettings = new ProviderSecuritySettings(); + foreach (var pair in this.AssociationLifetimes) { + securitySettings.AssociationLifetimes.Add(pair); + } + + securitySettings.MaximumHashBitLength = this.MaximumHashBitLength; + securitySettings.MinimumHashBitLength = this.MinimumHashBitLength; + securitySettings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks; + securitySettings.RequireSsl = this.RequireSsl; + securitySettings.SignOutgoingExtensions = this.SignOutgoingExtensions; + securitySettings.UnsolicitedAssertionVerification = this.UnsolicitedAssertionVerification; + + return securitySettings; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/RelyingPartyDiscoveryResult.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/RelyingPartyDiscoveryResult.cs new file mode 100644 index 0000000..4eca6d6 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/RelyingPartyDiscoveryResult.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="RelyingPartyDiscoveryResult.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + /// <summary> + /// The result codes that may be returned from an attempt at relying party discovery. + /// </summary> + public enum RelyingPartyDiscoveryResult { + /// <summary> + /// Relying Party discovery failed to find an XRDS document or the document was invalid. + /// </summary> + /// <remarks> + /// This can happen either when a relying party does not offer a service document at all, + /// or when a man-in-the-middle attack is in progress that prevents the Provider from being + /// able to discover that document. + /// </remarks> + NoServiceDocument, + + /// <summary> + /// Relying Party discovery yielded a valid XRDS document, but no matching return_to URI was found. + /// </summary> + /// <remarks> + /// This is perhaps the most dangerous rating for a relying party, since it suggests that + /// they are implementing OpenID 2.0 securely, but that a hijack operation may be in progress. + /// </remarks> + NoMatchingReturnTo, + + /// <summary> + /// Relying Party discovery succeeded, and a matching return_to URI was found. + /// </summary> + Success, + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/Request.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/Request.cs new file mode 100644 index 0000000..4e54ef9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/Request.cs @@ -0,0 +1,210 @@ +//----------------------------------------------------------------------- +// <copyright file="Request.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Implements the <see cref="IRequest"/> interface for all incoming + /// request messages to an OpenID Provider. + /// </summary> + [Serializable] + [ContractClass(typeof(RequestContract))] + [ContractVerification(true)] + internal abstract class Request : IRequest { + /// <summary> + /// The incoming request message. + /// </summary> + private readonly IDirectedProtocolMessage request; + + /// <summary> + /// The incoming request message cast to its extensible form. + /// Or null if the message does not support extensions. + /// </summary> + private readonly IProtocolMessageWithExtensions extensibleMessage; + + /// <summary> + /// The version of the OpenID protocol to use. + /// </summary> + private readonly Version protocolVersion; + + /// <summary> + /// Backing store for the <see cref="Protocol"/> property. + /// </summary> + [NonSerialized] + private Protocol protocol; + + /// <summary> + /// The list of extensions to add to the response message. + /// </summary> + private List<IOpenIdMessageExtension> responseExtensions = new List<IOpenIdMessageExtension>(); + + /// <summary> + /// Initializes a new instance of the <see cref="Request"/> class. + /// </summary> + /// <param name="request">The incoming request message.</param> + /// <param name="securitySettings">The security settings from the channel.</param> + protected Request(IDirectedProtocolMessage request, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + + this.request = request; + this.SecuritySettings = securitySettings; + this.protocolVersion = this.request.Version; + this.extensibleMessage = request as IProtocolMessageWithExtensions; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Request"/> class. + /// </summary> + /// <param name="version">The version.</param> + /// <param name="securitySettings">The security settings.</param> + protected Request(Version version, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(version != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + + this.protocolVersion = version; + this.SecuritySettings = securitySettings; + } + + #region IRequest Properties + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + public abstract bool IsResponseReady { get; } + + /// <summary> + /// Gets or sets the security settings that apply to this request. + /// </summary> + /// <value>Defaults to the <see cref="OpenIdProvider.SecuritySettings"/> on the <see cref="OpenIdProvider"/>.</value> + public ProviderSecuritySettings SecuritySettings { get; set; } + + /// <summary> + /// Gets the response to send to the user agent. + /// </summary> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IsResponseReady"/> is <c>false</c>.</exception> + internal IProtocolMessage Response { + get { + Contract.Requires<InvalidOperationException>(this.IsResponseReady, OpenIdStrings.ResponseNotReady); + Contract.Ensures(Contract.Result<IProtocolMessage>() != null); + + if (this.responseExtensions.Count > 0) { + var extensibleResponse = this.ResponseMessage as IProtocolMessageWithExtensions; + ErrorUtilities.VerifyOperation(extensibleResponse != null, MessagingStrings.MessageNotExtensible, this.ResponseMessage.GetType().Name); + foreach (var extension in this.responseExtensions) { + // It's possible that a prior call to this property + // has already added some/all of the extensions to the message. + // We don't have to worry about deleting old ones because + // this class provides no facility for removing extensions + // that are previously added. + if (!extensibleResponse.Extensions.Contains(extension)) { + extensibleResponse.Extensions.Add(extension); + } + } + } + + return this.ResponseMessage; + } + } + + #endregion + + /// <summary> + /// Gets the original request message. + /// </summary> + /// <value>This may be null in the case of an unrecognizable message.</value> + protected internal IDirectedProtocolMessage RequestMessage { + get { return this.request; } + } + + /// <summary> + /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>. + /// </summary> + protected abstract IProtocolMessage ResponseMessage { get; } + + /// <summary> + /// Gets the protocol version used in the request. + /// </summary> + protected Protocol Protocol { + get { + if (this.protocol == null) { + this.protocol = Protocol.Lookup(this.protocolVersion); + } + + return this.protocol; + } + } + + #region IRequest Methods + + /// <summary> + /// Adds an extension to the response to send to the relying party. + /// </summary> + /// <param name="extension">The extension to add to the response message.</param> + public void AddResponseExtension(IOpenIdMessageExtension extension) { + // Because the derived AuthenticationRequest class can swap out + // one response message for another (auth vs. no-auth), and because + // some response messages support extensions while others don't, + // we just add the extensions to a collection here and add them + // to the response on the way out. + this.responseExtensions.Add(extension); + } + + /// <summary> + /// Removes any response extensions previously added using <see cref="AddResponseExtension"/>. + /// </summary> + /// <remarks> + /// This should be called before sending a negative response back to the relying party + /// if extensions were already added, since negative responses cannot carry extensions. + /// </remarks> + public void ClearResponseExtensions() { + this.responseExtensions.Clear(); + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <typeparam name="T">The type of the extension.</typeparam> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + public T GetExtension<T>() where T : IOpenIdMessageExtension, new() { + if (this.extensibleMessage != null) { + return this.extensibleMessage.Extensions.OfType<T>().SingleOrDefault(); + } else { + return default(T); + } + } + + /// <summary> + /// Gets an extension sent from the relying party. + /// </summary> + /// <param name="extensionType">The type of the extension.</param> + /// <returns> + /// An instance of the extension initialized with values passed in with the request. + /// </returns> + public IOpenIdMessageExtension GetExtension(Type extensionType) { + if (this.extensibleMessage != null) { + return this.extensibleMessage.Extensions.OfType<IOpenIdMessageExtension>().Where(ext => extensionType.IsInstanceOfType(ext)).SingleOrDefault(); + } else { + return null; + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/RequestContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/RequestContract.cs new file mode 100644 index 0000000..dee140e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/RequestContract.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="RequestContract.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Code contract for the <see cref="Request"/> class. + /// </summary> + [ContractClassFor(typeof(Request))] + internal abstract class RequestContract : Request { + /// <summary> + /// Prevents a default instance of the <see cref="RequestContract"/> class from being created. + /// </summary> + private RequestContract() : base((Version)null, null) { + } + + /// <summary> + /// Gets a value indicating whether the response is ready to be sent to the user agent. + /// </summary> + /// <remarks> + /// This property returns false if there are properties that must be set on this + /// request instance before the response can be sent. + /// </remarks> + public override bool IsResponseReady { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the response message, once <see cref="IsResponseReady"/> is <c>true</c>. + /// </summary> + protected override IProtocolMessage ResponseMessage { + get { + Contract.Requires<InvalidOperationException>(this.IsResponseReady); + Contract.Ensures(Contract.Result<IProtocolMessage>() != null); + throw new NotImplementedException(); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/StandardProviderApplicationStore.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/StandardProviderApplicationStore.cs new file mode 100644 index 0000000..c13c4bc --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/StandardProviderApplicationStore.cs @@ -0,0 +1,117 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardProviderApplicationStore.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An in-memory store for Providers, suitable for single server, single process + /// ASP.NET web sites. + /// </summary> + /// <remarks> + /// This class provides only a basic implementation that is likely to work + /// out of the box on most single-server web sites. It is highly recommended + /// that high traffic web sites consider using a database to store the information + /// used by an OpenID Provider and write a custom implementation of the + /// <see cref="IOpenIdApplicationStore"/> interface to use instead of this + /// class. + /// </remarks> + public class StandardProviderApplicationStore : IOpenIdApplicationStore { + /// <summary> + /// The nonce store to use. + /// </summary> + private readonly INonceStore nonceStore; + + /// <summary> + /// The crypto key store where symmetric keys are persisted. + /// </summary> + private readonly ICryptoKeyStore cryptoKeyStore; + + /// <summary> + /// Initializes a new instance of the <see cref="StandardProviderApplicationStore"/> class. + /// </summary> + public StandardProviderApplicationStore() { + this.nonceStore = new NonceMemoryStore(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime); + this.cryptoKeyStore = new MemoryCryptoKeyStore(); + } + + #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 + + #region ICryptoKeyStore + + /// <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.cryptoKeyStore.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.cryptoKeyStore.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.cryptoKeyStore.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.cryptoKeyStore.RemoveKey(bucket, handle); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ProviderEndpointDescription.cs b/src/DotNetOpenAuth.OpenId/OpenId/ProviderEndpointDescription.cs new file mode 100644 index 0000000..6514ffd --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/Realm.cs b/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs new file mode 100644 index 0000000..98e3598 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs @@ -0,0 +1,500 @@ +//----------------------------------------------------------------------- +// <copyright file="Realm.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Text.RegularExpressions; + using System.Web; + using System.Xml; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Xrds; + using DotNetOpenAuth.Yadis; + + /// <summary> + /// A trust root to validate requests and match return URLs against. + /// </summary> + /// <remarks> + /// This fills the OpenID Authentication 2.0 specification for realms. + /// See http://openid.net/specs/openid-authentication-2_0.html#realms + /// </remarks> + [Serializable] + [Pure] + public class Realm { + /// <summary> + /// A regex used to detect a wildcard that is being used in the realm. + /// </summary> + private const string WildcardDetectionPattern = @"^(\w+://)\*\."; + + /// <summary> + /// A (more or less) comprehensive list of top-level (i.e. ".com") domains, + /// for use by <see cref="IsSane"/> in order to disallow overly-broad realms + /// that allow all web sites ending with '.com', for example. + /// </summary> + private static readonly string[] topLevelDomains = { "com", "edu", "gov", "int", "mil", "net", "org", "biz", "info", "name", "museum", "coop", "aero", "ac", "ad", "ae", + "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", + "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", + "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo", + "fr", "ga", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", + "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", + "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "mg", "mh", "mk", "ml", "mm", + "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", + "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru", "rw", "sa", + "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th", + "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", + "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw" }; + + /// <summary> + /// The Uri of the realm, with the wildcard (if any) removed. + /// </summary> + private Uri uri; + + /// <summary> + /// Initializes a new instance of the <see cref="Realm"/> class. + /// </summary> + /// <param name="realmUrl">The realm URL to use in the new instance.</param> + [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Not all realms are valid URLs (because of wildcards).")] + public Realm(string realmUrl) { + Contract.Requires<ArgumentNullException>(realmUrl != null); // not non-zero check so we throw UriFormatException later + this.DomainWildcard = Regex.IsMatch(realmUrl, WildcardDetectionPattern); + this.uri = new Uri(Regex.Replace(realmUrl, WildcardDetectionPattern, m => m.Groups[1].Value)); + if (!this.uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && + !this.uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { + throw new UriFormatException( + string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidScheme, this.uri.Scheme)); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="Realm"/> class. + /// </summary> + /// <param name="realmUrl">The realm URL of the Relying Party.</param> + public Realm(Uri realmUrl) { + Contract.Requires<ArgumentNullException>(realmUrl != null); + this.uri = realmUrl; + if (!this.uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && + !this.uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { + throw new UriFormatException( + string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidScheme, this.uri.Scheme)); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="Realm"/> class. + /// </summary> + /// <param name="realmUriBuilder">The realm URI builder.</param> + /// <remarks> + /// This is useful because UriBuilder can construct a host with a wildcard + /// in the Host property, but once there it can't be converted to a Uri. + /// </remarks> + internal Realm(UriBuilder realmUriBuilder) + : this(SafeUriBuilderToString(realmUriBuilder)) { } + + /// <summary> + /// Gets the suggested realm to use for the calling web application. + /// </summary> + /// <value>A realm that matches this applications root URL.</value> + /// <remarks> + /// <para>For most circumstances the Realm generated by this property is sufficient. + /// However a wildcard Realm, such as "http://*.microsoft.com/" may at times be more + /// desirable than "http://www.microsoft.com/" in order to allow identifier + /// correlation across related web sites for directed identity Providers.</para> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + public static Realm AutoDetect { + get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<Realm>() != null); + + HttpRequestInfo requestInfo = new HttpRequestInfo(HttpContext.Current.Request); + UriBuilder realmUrl = new UriBuilder(requestInfo.UrlBeforeRewriting); + realmUrl.Path = HttpContext.Current.Request.ApplicationPath; + realmUrl.Query = null; + realmUrl.Fragment = null; + + // For RP discovery, the realm url MUST NOT redirect. To prevent this for + // virtual directory hosted apps, we need to make sure that the realm path ends + // in a slash (since our calculation above guarantees it doesn't end in a specific + // page like default.aspx). + if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) { + realmUrl.Path += "/"; + } + + return realmUrl.Uri; + } + } + + /// <summary> + /// Gets a value indicating whether a '*.' prefix to the hostname is + /// used in the realm to allow subdomains or hosts to be added to the URL. + /// </summary> + public bool DomainWildcard { get; private set; } + + /// <summary> + /// Gets the host component of this instance. + /// </summary> + public string Host { + [DebuggerStepThrough] + get { return this.uri.Host; } + } + + /// <summary> + /// Gets the scheme name for this URI. + /// </summary> + public string Scheme { + [DebuggerStepThrough] + get { return this.uri.Scheme; } + } + + /// <summary> + /// Gets the port number of this URI. + /// </summary> + public int Port { + [DebuggerStepThrough] + get { return this.uri.Port; } + } + + /// <summary> + /// Gets the absolute path of the URI. + /// </summary> + public string AbsolutePath { + [DebuggerStepThrough] + get { return this.uri.AbsolutePath; } + } + + /// <summary> + /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated + /// by a question mark (?). + /// </summary> + public string PathAndQuery { + [DebuggerStepThrough] + get { return this.uri.PathAndQuery; } + } + + /// <summary> + /// Gets the original string. + /// </summary> + /// <value>The original string.</value> + internal string OriginalString { + get { return this.uri.OriginalString; } + } + + /// <summary> + /// Gets the realm URL. If the realm includes a wildcard, it is not included here. + /// </summary> + internal Uri NoWildcardUri { + [DebuggerStepThrough] + get { return this.uri; } + } + + /// <summary> + /// Gets the Realm discovery URL, where the wildcard (if present) is replaced with "www.". + /// </summary> + /// <remarks> + /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of + /// the "www" prefix. + /// </remarks> + internal Uri UriWithWildcardChangedToWww { + get { + if (this.DomainWildcard) { + UriBuilder builder = new UriBuilder(this.NoWildcardUri); + builder.Host = "www." + builder.Host; + return builder.Uri; + } else { + return this.NoWildcardUri; + } + } + } + + /// <summary> + /// Gets a value indicating whether this realm represents a reasonable (sane) set of URLs. + /// </summary> + /// <remarks> + /// 'http://*.com/', for example is not a reasonable pattern, as it cannot meaningfully + /// specify the site claiming it. This function attempts to find many related examples, + /// but it can only work via heuristics. Negative responses from this method should be + /// treated as advisory, used only to alert the user to examine the trust root carefully. + /// </remarks> + internal bool IsSane { + get { + if (this.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) { + return true; + } + + string[] host_parts = this.Host.Split('.'); + + string tld = host_parts[host_parts.Length - 1]; + + if (Array.IndexOf(topLevelDomains, tld) < 0) { + return false; + } + + if (tld.Length == 2) { + if (host_parts.Length == 1) { + return false; + } + + if (host_parts[host_parts.Length - 2].Length <= 3) { + return host_parts.Length > 2; + } + } else { + return host_parts.Length > 1; + } + + return false; + } + } + + /// <summary> + /// Implicitly converts the string-form of a URI to a <see cref="Realm"/> object. + /// </summary> + /// <param name="uri">The URI that the new Realm instance will represent.</param> + /// <returns>The result of the conversion.</returns> + [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Not all realms are valid URLs (because of wildcards).")] + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Not all Realms are valid URLs.")] + [DebuggerStepThrough] + public static implicit operator Realm(string uri) { + Contract.Ensures((Contract.Result<Realm>() != null) == (uri != null)); + return uri != null ? new Realm(uri) : null; + } + + /// <summary> + /// Implicitly converts a <see cref="Uri"/> to a <see cref="Realm"/> object. + /// </summary> + /// <param name="uri">The URI to convert to a realm.</param> + /// <returns>The result of the conversion.</returns> + [DebuggerStepThrough] + public static implicit operator Realm(Uri uri) { + Contract.Ensures((Contract.Result<Realm>() != null) == (uri != null)); + return uri != null ? new Realm(uri) : null; + } + + /// <summary> + /// Implicitly converts a <see cref="Realm"/> object to its <see cref="String"/> form. + /// </summary> + /// <param name="realm">The realm to convert to a string value.</param> + /// <returns>The result of the conversion.</returns> + [DebuggerStepThrough] + public static implicit operator string(Realm realm) { + Contract.Ensures((Contract.Result<string>() != null) == (realm != null)); + return realm != null ? realm.ToString() : null; + } + + /// <summary> + /// Checks whether one <see cref="Realm"/> is equal to another. + /// </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) { + Realm other = obj as Realm; + if (other == null) { + return false; + } + return this.uri.Equals(other.uri) && this.DomainWildcard == other.DomainWildcard; + } + + /// <summary> + /// Returns the hash code used for storing this object in a hash table. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return this.uri.GetHashCode() + (this.DomainWildcard ? 1 : 0); + } + + /// <summary> + /// Returns the string form of this <see cref="Realm"/>. + /// </summary> + /// <returns> + /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. + /// </returns> + public override string ToString() { + if (this.DomainWildcard) { + UriBuilder builder = new UriBuilder(this.uri); + builder.Host = "*." + builder.Host; + return builder.ToStringWithImpliedPorts(); + } else { + return this.uri.AbsoluteUri; + } + } + + /// <summary> + /// Validates a URL against this trust root. + /// </summary> + /// <param name="url">A string specifying URL to check.</param> + /// <returns>Whether the given URL is within this trust root.</returns> + internal bool Contains(string url) { + return this.Contains(new Uri(url)); + } + + /// <summary> + /// Validates a URL against this trust root. + /// </summary> + /// <param name="url">The URL to check.</param> + /// <returns>Whether the given URL is within this trust root.</returns> + internal bool Contains(Uri url) { + if (url.Scheme != this.Scheme) { + return false; + } + + if (url.Port != this.Port) { + return false; + } + + if (!this.DomainWildcard) { + if (url.Host != this.Host) { + return false; + } + } else { + Debug.Assert(!string.IsNullOrEmpty(this.Host), "The host part of the Regex should evaluate to at least one char for successful parsed trust roots."); + string[] host_parts = this.Host.Split('.'); + string[] url_parts = url.Host.Split('.'); + + // If the domain containing the wildcard has more parts than the URL to match against, + // it naturally can't be valid. + // Unless *.example.com actually matches example.com too. + if (host_parts.Length > url_parts.Length) { + return false; + } + + // Compare last part first and move forward. + // Maybe could be done by using EndsWith, but piecewies helps ensure that + // *.my.com doesn't match ohmeohmy.com but can still match my.com. + for (int i = 0; i < host_parts.Length; i++) { + string hostPart = host_parts[host_parts.Length - 1 - i]; + string urlPart = url_parts[url_parts.Length - 1 - i]; + if (!string.Equals(hostPart, urlPart, StringComparison.OrdinalIgnoreCase)) { + return false; + } + } + } + + // If path matches or is specified to root ... + // (deliberately case sensitive to protect security on case sensitive systems) + if (this.PathAndQuery.Equals(url.PathAndQuery, StringComparison.Ordinal) + || this.PathAndQuery.Equals("/", StringComparison.Ordinal)) { + return true; + } + + // If trust root has a longer path, the return URL must be invalid. + if (this.PathAndQuery.Length > url.PathAndQuery.Length) { + return false; + } + + // The following code assures that http://example.com/directory isn't below http://example.com/dir, + // but makes sure http://example.com/dir/ectory is below http://example.com/dir + int path_len = this.PathAndQuery.Length; + string url_prefix = url.PathAndQuery.Substring(0, path_len); + + if (this.PathAndQuery != url_prefix) { + return false; + } + + // If trust root includes a query string ... + if (this.PathAndQuery.Contains("?")) { + // ... make sure return URL begins with a new argument + return url.PathAndQuery[path_len] == '&'; + } + + // Or make sure a query string is introduced or a path below trust root + return this.PathAndQuery.EndsWith("/", StringComparison.Ordinal) + || url.PathAndQuery[path_len] == '?' + || url.PathAndQuery[path_len] == '/'; + } + + /// <summary> + /// Searches for an XRDS document at the realm URL, and if found, searches + /// for a description of a relying party endpoints (OpenId login pages). + /// </summary> + /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param> + /// <param name="allowRedirects">Whether redirects may be followed when discovering the Realm. + /// This may be true when creating an unsolicited assertion, but must be + /// false when performing return URL verification per 2.0 spec section 9.2.1.</param> + /// <returns> + /// The details of the endpoints if found; or <c>null</c> if no service document was discovered. + /// </returns> + internal virtual IEnumerable<RelyingPartyEndpointDescription> DiscoverReturnToEndpoints(IDirectWebRequestHandler requestHandler, bool allowRedirects) { + XrdsDocument xrds = this.Discover(requestHandler, allowRedirects); + if (xrds != null) { + return xrds.FindRelyingPartyReceivingEndpoints(); + } + + return null; + } + + /// <summary> + /// Searches for an XRDS document at the realm URL. + /// </summary> + /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param> + /// <param name="allowRedirects">Whether redirects may be followed when discovering the Realm. + /// This may be true when creating an unsolicited assertion, but must be + /// false when performing return URL verification per 2.0 spec section 9.2.1.</param> + /// <returns> + /// The XRDS document if found; or <c>null</c> if no service document was discovered. + /// </returns> + internal virtual XrdsDocument Discover(IDirectWebRequestHandler requestHandler, bool allowRedirects) { + // Attempt YADIS discovery + DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this.UriWithWildcardChangedToWww, false); + if (yadisResult != null) { + // Detect disallowed redirects, since realm discovery never allows them for security. + ErrorUtilities.VerifyProtocol(allowRedirects || yadisResult.NormalizedUri == yadisResult.RequestUri, OpenIdStrings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri); + if (yadisResult.IsXrds) { + try { + return new XrdsDocument(yadisResult.ResponseText); + } catch (XmlException ex) { + throw ErrorUtilities.Wrap(ex, XrdsStrings.InvalidXRDSDocument); + } + } + } + + return null; + } + + /// <summary> + /// Calls <see cref="UriBuilder.ToString"/> if the argument is non-null. + /// Otherwise throws <see cref="ArgumentNullException"/>. + /// </summary> + /// <param name="realmUriBuilder">The realm URI builder.</param> + /// <returns>The result of UriBuilder.ToString()</returns> + /// <remarks> + /// This simple method is worthwhile because it checks for null + /// before dereferencing the UriBuilder. Since this is called from + /// within a constructor's base(...) call, this avoids a <see cref="NullReferenceException"/> + /// when we should be throwing an <see cref="ArgumentNullException"/>. + /// </remarks> + private static string SafeUriBuilderToString(UriBuilder realmUriBuilder) { + Contract.Requires<ArgumentNullException>(realmUriBuilder != null); + + // Note: we MUST use ToString. Uri property throws if wildcard is present. + // Note that Uri.ToString() should generally be avoided, but UriBuilder.ToString() + // is safe: http://blog.nerdbank.net/2008/04/uriabsoluteuri-and-uritostring-are-not.html + return realmUriBuilder.ToString(); + } + +#if CONTRACTS_FULL + /// <summary> + /// Verifies conditions that should be true for any valid state of this object. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] + [ContractInvariantMethod] + private void ObjectInvariant() { + Contract.Invariant(this.uri != null); + Contract.Invariant(this.uri.AbsoluteUri != null); + } +#endif + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs new file mode 100644 index 0000000..9a43506 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/AssociationPreference.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationPreference.cs new file mode 100644 index 0000000..9f4a21f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/Associations.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/Associations.cs new file mode 100644 index 0000000..b171bec --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AuthenticationRequest.cs new file mode 100644 index 0000000..8bbf04f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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 && DotNetOpenAuthSection.Configuration.OpenId.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/OpenId/RelyingParty/AuthenticationRequestMode.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AuthenticationRequestMode.cs new file mode 100644 index 0000000..70b7f3a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AuthenticationRequestMode.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationRequestMode.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + /// <summary> + /// Indicates the mode the Provider should use while authenticating the end user. + /// </summary> + public enum AuthenticationRequestMode { + /// <summary> + /// The Provider should use whatever credentials are immediately available + /// to determine whether the end user owns the Identifier. If sufficient + /// credentials (i.e. cookies) are not immediately available, the Provider + /// should fail rather than prompt the user. + /// </summary> + Immediate, + + /// <summary> + /// The Provider should determine whether the end user owns the Identifier, + /// displaying a web page to the user to login etc., if necessary. + /// </summary> + Setup, + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AuthenticationStatus.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AuthenticationStatus.cs new file mode 100644 index 0000000..d9e5d0a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/Controls.cd b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/Controls.cd new file mode 100644 index 0000000..f96db36 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/CryptoKeyStoreAsRelyingPartyAssociationStore.cs new file mode 100644 index 0000000..02ed3b0 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs new file mode 100644 index 0000000..94eb5ba --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/FailedAuthenticationResponse.cs new file mode 100644 index 0000000..682e3ff --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/IAuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/IAuthenticationRequest.cs new file mode 100644 index 0000000..65db0bd --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/IAuthenticationRequestContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/IAuthenticationRequestContract.cs new file mode 100644 index 0000000..cd36cc7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/IAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/IAuthenticationResponse.cs new file mode 100644 index 0000000..a24220f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/IProviderEndpoint.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/IProviderEndpoint.cs new file mode 100644 index 0000000..5d8918d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/IRelyingPartyAssociationStore.cs new file mode 100644 index 0000000..21a2c53 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/IRelyingPartyBehavior.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/IRelyingPartyBehavior.cs new file mode 100644 index 0000000..1bfa0db --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs new file mode 100644 index 0000000..cfbccef --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/NegativeAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/NegativeAuthenticationResponse.cs new file mode 100644 index 0000000..9e3824d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs new file mode 100644 index 0000000..652fa6b --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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 (DotNetOpenAuthSection.Configuration.OpenId.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/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdAjaxTextBox.cs new file mode 100644 index 0000000..8be097f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdAjaxTextBox.css b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdAjaxTextBox.css new file mode 100644 index 0000000..bed2e79 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdAjaxTextBox.js new file mode 100644 index 0000000..9907b4e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdButton.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdButton.cs new file mode 100644 index 0000000..6243917 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdEventArgs.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdEventArgs.cs new file mode 100644 index 0000000..5668cf4 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdLogin.cs new file mode 100644 index 0000000..eccdacf --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdMobileTextBox.cs new file mode 100644 index 0000000..9cafb74 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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 ?? DotNetOpenAuthSection.Configuration.OpenId.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/OpenId/RelyingParty/OpenIdRelyingParty.cd b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cd new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cd @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs new file mode 100644 index 0000000..aec59b8 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. + /// </summary> + /// <param name="applicationStore">The application store. If <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 = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings(); + + foreach (var discoveryService in DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.DiscoveryServices.CreateInstances(true)) { + this.discoveryServices.Add(discoveryService); + } + + this.behaviors.CollectionChanged += this.OnBehaviorsChanged; + foreach (var behavior in DotNetOpenAuthSection.Configuration.OpenId.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/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs new file mode 100644 index 0000000..551534a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Configuration.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/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js new file mode 100644 index 0000000..4de5188 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs new file mode 100644 index 0000000..b1106e6 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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 : DotNetOpenAuthSection.Configuration.OpenId.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/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.js new file mode 100644 index 0000000..58b283d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdSelector.cs new file mode 100644 index 0000000..538e181 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdSelector.css b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdSelector.css new file mode 100644 index 0000000..e7eafc7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdSelector.js b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdSelector.js new file mode 100644 index 0000000..297ea23 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdTextBox.cs new file mode 100644 index 0000000..335b435 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/PopupBehavior.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/PopupBehavior.cs new file mode 100644 index 0000000..e84f4f5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/PositiveAnonymousResponse.cs new file mode 100644 index 0000000..fc334b0 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/PositiveAuthenticationResponse.cs new file mode 100644 index 0000000..3e2298c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/PositiveAuthenticationResponseSnapshot.cs new file mode 100644 index 0000000..80b424a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/RelyingPartySecuritySettings.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/RelyingPartySecuritySettings.cs new file mode 100644 index 0000000..fc6d4c7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/SelectorButton.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/SelectorButton.cs new file mode 100644 index 0000000..0be3a5f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/SelectorButtonContract.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/SelectorButtonContract.cs new file mode 100644 index 0000000..c70218a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/SelectorInfoCardButton.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/SelectorInfoCardButton.cs new file mode 100644 index 0000000..c5dda1c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/SelectorOpenIdButton.cs new file mode 100644 index 0000000..ac4dcbf --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/SelectorProviderButton.cs new file mode 100644 index 0000000..2195e73 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs new file mode 100644 index 0000000..678f69a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs new file mode 100644 index 0000000..f17b260 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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(DotNetOpenAuthSection.Configuration.OpenId.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/OpenId/RelyingParty/WellKnownProviders.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/WellKnownProviders.cs new file mode 100644 index 0000000..ad1a11a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/RelyingParty/login_failure.png b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_failure.png Binary files differnew file mode 100644 index 0000000..8003700 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_failure.png diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_success (lock).png b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_success (lock).png Binary files differnew file mode 100644 index 0000000..bc0c0c8 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_success (lock).png diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_success.png b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_success.png Binary files differnew file mode 100644 index 0000000..0ae1365 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/login_success.png diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/openid_login.png b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/openid_login.png Binary files differnew file mode 100644 index 0000000..caebd58 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/openid_login.png diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/spinner.gif b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/spinner.gif Binary files differnew file mode 100644 index 0000000..9cb298e --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/spinner.gif diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingPartyDescription.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingPartyDescription.cs new file mode 100644 index 0000000..7926e8f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingPartyDescription.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------- +// <copyright file="RelyingPartyDescription.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; + + /// <summary> + /// A description of some OpenID Relying Party endpoint. + /// </summary> + /// <remarks> + /// This is an immutable type. + /// </remarks> + internal class RelyingPartyEndpointDescription { + /// <summary> + /// Initializes a new instance of the <see cref="RelyingPartyEndpointDescription"/> class. + /// </summary> + /// <param name="returnTo">The return to.</param> + /// <param name="supportedServiceTypeUris"> + /// The Type URIs of supported services advertised on a relying party's XRDS document. + /// </param> + internal RelyingPartyEndpointDescription(Uri returnTo, string[] supportedServiceTypeUris) { + Contract.Requires<ArgumentNullException>(returnTo != null); + Contract.Requires<ArgumentNullException>(supportedServiceTypeUris != null); + + this.ReturnToEndpoint = returnTo; + this.Protocol = GetProtocolFromServices(supportedServiceTypeUris); + } + + /// <summary> + /// Gets the URL to the login page on the discovered relying party web site. + /// </summary> + public Uri ReturnToEndpoint { get; private set; } + + /// <summary> + /// Gets the OpenId protocol that the discovered relying party supports. + /// </summary> + public Protocol Protocol { get; private set; } + + /// <summary> + /// Derives the highest OpenID protocol that this library and the OpenID Provider have + /// in common. + /// </summary> + /// <param name="supportedServiceTypeUris">The supported service type URIs.</param> + /// <returns>The best OpenID protocol version to use when communicating with this Provider.</returns> + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "OpenID", Justification = "Spelling correct")] + private static Protocol GetProtocolFromServices(string[] supportedServiceTypeUris) { + Protocol protocol = Protocol.FindBestVersion(p => p.RPReturnToTypeURI, supportedServiceTypeUris); + if (protocol == null) { + throw new InvalidOperationException("Unable to determine the version of OpenID the Relying Party supports."); + } + return protocol; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/SecuritySettings.cs b/src/DotNetOpenAuth.OpenId/OpenId/SecuritySettings.cs new file mode 100644 index 0000000..26f6d2a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/SecuritySettings.cs @@ -0,0 +1,95 @@ +//----------------------------------------------------------------------- +// <copyright file="SecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Security settings that may be applicable to both relying parties and providers. + /// </summary> + [Serializable] + public abstract class SecuritySettings { + /// <summary> + /// Gets the default minimum hash bit length. + /// </summary> + internal const int MinimumHashBitLengthDefault = 160; + + /// <summary> + /// Gets the maximum hash bit length default for relying parties. + /// </summary> + internal const int MaximumHashBitLengthRPDefault = 256; + + /// <summary> + /// Gets the maximum hash bit length default for providers. + /// </summary> + internal const int MaximumHashBitLengthOPDefault = 512; + + /// <summary> + /// Initializes a new instance of the <see cref="SecuritySettings"/> class. + /// </summary> + /// <param name="isProvider">A value indicating whether this class is being instantiated for a Provider.</param> + protected SecuritySettings(bool isProvider) { + this.MaximumHashBitLength = isProvider ? MaximumHashBitLengthOPDefault : MaximumHashBitLengthRPDefault; + this.MinimumHashBitLength = MinimumHashBitLengthDefault; + } + + /// <summary> + /// Gets or sets the minimum hash length (in bits) allowed to be used in an <see cref="Association"/> + /// with the remote party. The default is 160. + /// </summary> + /// <remarks> + /// SHA-1 (160 bits) has been broken. The minimum secure hash length is now 256 bits. + /// The default is still a 160 bit minimum to allow interop with common remote parties, + /// such as Yahoo! that only supports 160 bits. + /// For sites that require high security such as to store bank account information and + /// health records, 256 is the recommended value. + /// </remarks> + public int MinimumHashBitLength { get; set; } + + /// <summary> + /// Gets or sets the maximum hash length (in bits) allowed to be used in an <see cref="Association"/> + /// with the remote party. The default is 256 for relying parties and 512 for providers. + /// </summary> + /// <remarks> + /// The longer the bit length, the more secure the identities of your visitors are. + /// Setting a value higher than 256 on a relying party site may reduce performance + /// as many association requests will be denied, causing secondary requests or even + /// authentication failures. + /// Setting a value higher than 256 on a provider increases security where possible + /// without these side-effects. + /// </remarks> + public int MaximumHashBitLength { get; set; } + + /// <summary> + /// Determines whether a named association fits the security requirements. + /// </summary> + /// <param name="protocol">The protocol carrying the association.</param> + /// <param name="associationType">The value of the openid.assoc_type parameter.</param> + /// <returns> + /// <c>true</c> if the association is permitted given the security requirements; otherwise, <c>false</c>. + /// </returns> + internal bool IsAssociationInPermittedRange(Protocol protocol, string associationType) { + int lengthInBits = HmacShaAssociation.GetSecretLength(protocol, associationType) * 8; + return lengthInBits >= this.MinimumHashBitLength && lengthInBits <= this.MaximumHashBitLength; + } + + /// <summary> + /// Determines whether a given association fits the security requirements. + /// </summary> + /// <param name="association">The association to check.</param> + /// <returns> + /// <c>true</c> if the association is permitted given the security requirements; otherwise, <c>false</c>. + /// </returns> + internal bool IsAssociationInPermittedRange(Association association) { + Contract.Requires<ArgumentNullException>(association != null); + return association.HashBitLength >= this.MinimumHashBitLength && association.HashBitLength <= this.MaximumHashBitLength; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/UriDiscoveryService.cs b/src/DotNetOpenAuth.OpenId/OpenId/UriDiscoveryService.cs new file mode 100644 index 0000000..7d17fd9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs new file mode 100644 index 0000000..145a394 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs @@ -0,0 +1,727 @@ +//----------------------------------------------------------------------- +// <copyright file="UriIdentifier.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.Reflection; + using System.Security; + 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> + /// A URI style of OpenID Identifier. + /// </summary> + [Serializable] + [Pure] + public sealed class UriIdentifier : Identifier { + /// <summary> + /// The allowed protocol schemes in a URI Identifier. + /// </summary> + private static readonly string[] allowedSchemes = { "http", "https" }; + + /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// A value indicating whether scheme substitution is being used to workaround + /// .NET path compression that invalidates some OpenIDs that have trailing periods + /// in one of their path segments. + /// </summary> + private static bool schemeSubstitution; + + /// <summary> + /// Initializes static members of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <remarks> + /// This method attempts to workaround the .NET Uri class parsing bug described here: + /// https://connect.microsoft.com/VisualStudio/feedback/details/386695/system-uri-incorrectly-strips-trailing-dots?wa=wsignin1.0#tabs + /// since some identifiers (like some of the pseudonymous identifiers from Yahoo) include path segments + /// that end with periods, which the Uri class will typically trim off. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Some things just can't be done in a field initializer.")] + static UriIdentifier() { + // Our first attempt to handle trailing periods in path segments is to leverage + // full trust if it's available to rewrite the rules. + // In fact this is the ONLY way in .NET 3.5 (and arguably in .NET 4.0) to send + // outbound HTTP requests with trailing periods, so it's the only way to perform + // discovery on such an identifier. + try { + UriParser.Register(roundTrippingHttpParser, "dnoarthttp", 80); + UriParser.Register(roundTrippingHttpsParser, "dnoarthttps", 443); + UriParser.Register(publishableHttpParser, "dnoahttp", 80); + UriParser.Register(publishableHttpsParser, "dnoahttps", 443); + roundTrippingHttpParser.Initialize(false); + roundTrippingHttpsParser.Initialize(false); + publishableHttpParser.Initialize(true); + publishableHttpsParser.Initialize(true); + schemeSubstitution = true; + Logger.OpenId.Debug(".NET Uri class path compression overridden."); + Reporting.RecordFeatureUse("FullTrust"); + } catch (SecurityException) { + // We must be running in partial trust. Nothing more we can do. + Logger.OpenId.Warn("Unable to coerce .NET to stop compressing URI paths due to partial trust limitations. Some URL identifiers may be unable to complete login."); + Reporting.RecordFeatureUse("PartialTrust"); + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + internal UriIdentifier(string uri) + : this(uri, false) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(uri)); + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + /// <param name="requireSslDiscovery">if set to <c>true</c> [require SSL discovery].</param> + internal UriIdentifier(string uri, bool requireSslDiscovery) + : base(uri, requireSslDiscovery) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(uri)); + Uri canonicalUri; + bool schemePrepended; + if (!TryCanonicalize(uri, out canonicalUri, requireSslDiscovery, out schemePrepended)) { + throw new UriFormatException(); + } + if (requireSslDiscovery && canonicalUri.Scheme != Uri.UriSchemeHttps) { + throw new ArgumentException(OpenIdStrings.ExplicitHttpUriSuppliedWithSslRequirement); + } + this.Uri = canonicalUri; + this.SchemeImplicitlyPrepended = schemePrepended; + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + internal UriIdentifier(Uri uri) + : this(uri, false) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <param name="uri">The value this identifier will represent.</param> + /// <param name="requireSslDiscovery">if set to <c>true</c> [require SSL discovery].</param> + internal UriIdentifier(Uri uri, bool requireSslDiscovery) + : base(uri != null ? uri.OriginalString : null, requireSslDiscovery) { + Contract.Requires<ArgumentNullException>(uri != null); + + string uriAsString = uri.OriginalString; + if (schemeSubstitution) { + uriAsString = NormalSchemeToSpecialRoundTrippingScheme(uriAsString); + } + + if (!TryCanonicalize(uriAsString, out uri)) { + throw new UriFormatException(); + } + if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) { + throw new ArgumentException(OpenIdStrings.ExplicitHttpUriSuppliedWithSslRequirement); + } + this.Uri = uri; + this.SchemeImplicitlyPrepended = false; + } + + /// <summary> + /// Gets or sets a value indicating whether scheme substitution is being used to workaround + /// .NET path compression that invalidates some OpenIDs that have trailing periods + /// in one of their path segments. + /// </summary> + internal static bool SchemeSubstitutionTestHook { + get { return schemeSubstitution; } + set { schemeSubstitution = value; } + } + + /// <summary> + /// Gets the URI this instance represents. + /// </summary> + internal Uri Uri { get; private set; } + + /// <summary> + /// Gets a value indicating whether the scheme was missing when this + /// Identifier was created and added automatically as part of the + /// normalization process. + /// </summary> + internal bool SchemeImplicitlyPrepended { get; private set; } + + /// <summary> + /// Gets a value indicating whether this Identifier has characters or patterns that + /// the <see cref="Uri"/> class normalizes away and invalidating the Identifier. + /// </summary> + internal bool ProblematicNormalization { + get { + if (schemeSubstitution) { + // With full trust, we have no problematic URIs + return false; + } + + var simpleUri = new SimpleUri(this.OriginalString); + if (simpleUri.Path.EndsWith(".", StringComparison.Ordinal) || simpleUri.Path.Contains("./")) { + return true; + } + + return false; + } + } + + /// <summary> + /// Converts a <see cref="UriIdentifier"/> instance to a <see cref="Uri"/> instance. + /// </summary> + /// <param name="identifier">The identifier to convert to an ordinary <see cref="Uri"/> instance.</param> + /// <returns>The result of the conversion.</returns> + public static implicit operator Uri(UriIdentifier identifier) { + if (identifier == null) { + return null; + } + return identifier.Uri; + } + + /// <summary> + /// Converts a <see cref="Uri"/> instance to a <see cref="UriIdentifier"/> instance. + /// </summary> + /// <param name="identifier">The <see cref="Uri"/> instance to turn into a <see cref="UriIdentifier"/>.</param> + /// <returns>The result of the conversion.</returns> + public static implicit operator UriIdentifier(Uri identifier) { + if (identifier == null) { + return null; + } + return new UriIdentifier(identifier); + } + + /// <summary> + /// Tests equality between this URI and another URI. + /// </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) { + UriIdentifier other = obj as UriIdentifier; + if (obj != null && other == null && Identifier.EqualityOnStrings) { // test hook to enable MockIdentifier comparison + other = Identifier.Parse(obj.ToString()) as UriIdentifier; + } + if (other == null) { + return false; + } + + if (this.ProblematicNormalization || other.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).Equals(new SimpleUri(other.OriginalString)); + } else { + return this.Uri == other.Uri; + } + } + + /// <summary> + /// Returns the hash code of this XRI. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return Uri.GetHashCode(); + } + + /// <summary> + /// Returns the string form of the URI. + /// </summary> + /// <returns> + /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. + /// </returns> + public override string ToString() { + if (this.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).ToString(); + } else { + return this.Uri.AbsoluteUri; + } + } + + /// <summary> + /// Determines whether a URI is a valid OpenID Identifier (of any kind). + /// </summary> + /// <param name="uri">The URI to test for OpenID validity.</param> + /// <returns> + /// <c>true</c> if the identifier is valid; otherwise, <c>false</c>. + /// </returns> + /// <remarks> + /// A valid URI is absolute (not relative) and uses an http(s) scheme. + /// </remarks> + internal static bool IsValidUri(string uri) { + Uri normalized; + bool schemePrepended; + return TryCanonicalize(uri, out normalized, false, out schemePrepended); + } + + /// <summary> + /// Determines whether a URI is a valid OpenID Identifier (of any kind). + /// </summary> + /// <param name="uri">The URI to test for OpenID validity.</param> + /// <returns> + /// <c>true</c> if the identifier is valid; otherwise, <c>false</c>. + /// </returns> + /// <remarks> + /// A valid URI is absolute (not relative) and uses an http(s) scheme. + /// </remarks> + internal static bool IsValidUri(Uri uri) { + if (uri == null) { + return false; + } + if (!uri.IsAbsoluteUri) { + return false; + } + if (!IsAllowedScheme(uri)) { + return false; + } + return true; + } + + /// <summary> + /// Returns an <see cref="Identifier"/> that has no URI fragment. + /// Quietly returns the original <see cref="Identifier"/> if it is not + /// a <see cref="UriIdentifier"/> or no fragment exists. + /// </summary> + /// <returns> + /// A new <see cref="Identifier"/> instance if there was a + /// fragment to remove, otherwise this same instance.. + /// </returns> + internal override Identifier TrimFragment() { + // If there is no fragment, we have no need to rebuild the Identifier. + if (Uri.Fragment == null || Uri.Fragment.Length == 0) { + return this; + } + + // Strip the fragment. + return new UriIdentifier(this.OriginalString.Substring(0, this.OriginalString.IndexOf('#'))); + } + + /// <summary> + /// Converts a given identifier to its secure equivalent. + /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS. + /// Discovery is made to require SSL for the entire resolution process. + /// </summary> + /// <param name="secureIdentifier">The newly created secure identifier. + /// If the conversion fails, <paramref name="secureIdentifier"/> retains + /// <i>this</i> identifiers identity, but will never discover any endpoints.</param> + /// <returns> + /// True if the secure conversion was successful. + /// False if the Identifier was originally created with an explicit HTTP scheme. + /// </returns> + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + // If this Identifier is already secure, reuse it. + if (IsDiscoverySecureEndToEnd) { + secureIdentifier = this; + return true; + } + + // If this identifier already uses SSL for initial discovery, return one + // that guarantees it will be used throughout the discovery process. + if (String.Equals(Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + secureIdentifier = new UriIdentifier(this.Uri, true); + return true; + } + + // Otherwise, try to make this Identifier secure by normalizing to HTTPS instead of HTTP. + if (this.SchemeImplicitlyPrepended) { + UriBuilder newIdentifierUri = new UriBuilder(this.Uri); + newIdentifierUri.Scheme = Uri.UriSchemeHttps; + if (newIdentifierUri.Port == 80) { + newIdentifierUri.Port = 443; + } + secureIdentifier = new UriIdentifier(newIdentifierUri.Uri, true); + return true; + } + + // This identifier is explicitly NOT https, so we cannot change it. + secureIdentifier = new NoDiscoveryIdentifier(this, true); + return false; + } + + /// <summary> + /// Determines whether the given URI is using a scheme in the list of allowed schemes. + /// </summary> + /// <param name="uri">The URI whose scheme is to be checked.</param> + /// <returns> + /// <c>true</c> if the scheme is allowed; otherwise, <c>false</c>. + /// <c>false</c> is also returned if <paramref name="uri"/> is null. + /// </returns> + private static bool IsAllowedScheme(string uri) { + if (string.IsNullOrEmpty(uri)) { + return false; + } + return Array.FindIndex( + allowedSchemes, + s => uri.StartsWith(s + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase)) >= 0; + } + + /// <summary> + /// Determines whether the given URI is using a scheme in the list of allowed schemes. + /// </summary> + /// <param name="uri">The URI whose scheme is to be checked.</param> + /// <returns> + /// <c>true</c> if the scheme is allowed; otherwise, <c>false</c>. + /// <c>false</c> is also returned if <paramref name="uri"/> is null. + /// </returns> + private static bool IsAllowedScheme(Uri uri) { + if (uri == null) { + return false; + } + return Array.FindIndex( + allowedSchemes, + s => uri.Scheme.Equals(s, StringComparison.OrdinalIgnoreCase)) >= 0; + } + + /// <summary> + /// Tries to canonicalize a user-supplied identifier. + /// This does NOT convert a user-supplied identifier to a Claimed Identifier! + /// </summary> + /// <param name="uri">The user-supplied identifier.</param> + /// <param name="canonicalUri">The resulting canonical URI.</param> + /// <param name="forceHttpsDefaultScheme">If set to <c>true</c> and the user-supplied identifier lacks a scheme, the "https://" scheme will be prepended instead of the standard "http://" one.</param> + /// <param name="schemePrepended">if set to <c>true</c> [scheme prepended].</param> + /// <returns> + /// <c>true</c> if the identifier was valid and could be canonicalized. + /// <c>false</c> if the identifier is outside the scope of allowed inputs and should be rejected. + /// </returns> + /// <remarks> + /// Canonicalization is done by adding a scheme in front of an + /// identifier if it isn't already present. Other trivial changes that do not + /// require network access are also done, such as lower-casing the hostname in the URI. + /// </remarks> + private static bool TryCanonicalize(string uri, out Uri canonicalUri, bool forceHttpsDefaultScheme, out bool schemePrepended) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(uri)); + + canonicalUri = null; + try { + uri = DoSimpleCanonicalize(uri, forceHttpsDefaultScheme, out schemePrepended); + if (schemeSubstitution) { + uri = NormalSchemeToSpecialRoundTrippingScheme(uri); + } + + // Use a UriBuilder because it helps to normalize the URL as well. + return TryCanonicalize(uri, out canonicalUri); + } catch (UriFormatException) { + // We try not to land here with checks in the try block, but just in case. + schemePrepended = false; + return false; + } + } + + /// <summary> + /// Fixes up the scheme if appropriate. + /// </summary> + /// <param name="uri">The URI, already in legal form (with http(s):// prepended if necessary).</param> + /// <param name="canonicalUri">The resulting canonical URI.</param> + /// <returns><c>true</c> if the canonicalization was successful; <c>false</c> otherwise.</returns> + /// <remarks> + /// This does NOT standardize an OpenID URL for storage in a database, as + /// it does nothing to convert the URL to a Claimed Identifier, besides the fact + /// that it only deals with URLs whereas OpenID 2.0 supports XRIs. + /// For this, you should lookup the value stored in IAuthenticationResponse.ClaimedIdentifier. + /// </remarks> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "The user will see the result of this operation and they want to see it in lower case.")] + private static bool TryCanonicalize(string uri, out Uri canonicalUri) { + Contract.Requires<ArgumentNullException>(uri != null); + + if (schemeSubstitution) { + UriBuilder uriBuilder = new UriBuilder(uri); + + // Swap out our round-trippable scheme for the publishable (hidden) scheme. + uriBuilder.Scheme = uriBuilder.Scheme == roundTrippingHttpParser.RegisteredScheme ? publishableHttpParser.RegisteredScheme : publishableHttpsParser.RegisteredScheme; + canonicalUri = uriBuilder.Uri; + } else { + canonicalUri = new Uri(uri); + } + + return true; + } + + /// <summary> + /// Gets the special non-compressing scheme or URL for a standard scheme or URL. + /// </summary> + /// <param name="normal">The ordinary URL or scheme name.</param> + /// <returns>The non-compressing equivalent scheme or URL for the given value.</returns> + private static string NormalSchemeToSpecialRoundTrippingScheme(string normal) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(normal)); + Contract.Requires<InternalErrorException>(schemeSubstitution); + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + + int delimiterIndex = normal.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + string normalScheme = delimiterIndex < 0 ? normal : normal.Substring(0, delimiterIndex); + string nonCompressingScheme; + if (string.Equals(normalScheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpParser.RegisteredScheme; + } else if (string.Equals(normalScheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpsParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpsParser.RegisteredScheme; + } else { + throw new NotSupportedException(); + } + + return delimiterIndex < 0 ? nonCompressingScheme : nonCompressingScheme + normal.Substring(delimiterIndex); + } + + /// <summary> + /// Performs the minimal URL normalization to allow a string to be passed to the <see cref="Uri"/> constructor. + /// </summary> + /// <param name="uri">The user-supplied identifier URI to normalize.</param> + /// <param name="forceHttpsDefaultScheme">if set to <c>true</c>, a missing scheme should result in HTTPS being prepended instead of HTTP.</param> + /// <param name="schemePrepended">if set to <c>true</c>, the scheme was prepended during normalization.</param> + /// <returns>The somewhat normalized URL.</returns> + private static string DoSimpleCanonicalize(string uri, bool forceHttpsDefaultScheme, out bool schemePrepended) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(uri)); + + schemePrepended = false; + uri = uri.Trim(); + + // Assume http:// scheme if an allowed scheme isn't given, and strip + // fragments off. Consistent with spec section 7.2#3 + if (!IsAllowedScheme(uri)) { + uri = (forceHttpsDefaultScheme ? Uri.UriSchemeHttps : Uri.UriSchemeHttp) + + Uri.SchemeDelimiter + uri; + schemePrepended = true; + } + + return uri; + } + +#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.Uri != null); + Contract.Invariant(this.Uri.AbsoluteUri != null); + } +#endif + + /// <summary> + /// A simple URI class that doesn't suffer from the parsing problems of the <see cref="Uri"/> class. + /// </summary> + internal class SimpleUri { + /// <summary> + /// URI characters that separate the URI Path from subsequent elements. + /// </summary> + private static readonly char[] PathEndingCharacters = new char[] { '?', '#' }; + + /// <summary> + /// Initializes a new instance of the <see cref="SimpleUri"/> class. + /// </summary> + /// <param name="value">The value.</param> + internal SimpleUri(string value) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(value)); + + bool schemePrepended; + value = DoSimpleCanonicalize(value, false, out schemePrepended); + + // Leverage the Uri class's parsing where we can. + Uri uri = new Uri(value); + this.Scheme = uri.Scheme; + this.Authority = uri.Authority; + this.Query = uri.Query; + this.Fragment = uri.Fragment; + + // Get the Path out ourselves, since the default Uri parser compresses it too much for OpenID. + int schemeLength = value.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + Contract.Assume(schemeLength > 0); + int hostStart = schemeLength + Uri.SchemeDelimiter.Length; + int hostFinish = value.IndexOf('/', hostStart); + if (hostFinish < 0) { + this.Path = "/"; + } else { + int pathFinish = value.IndexOfAny(PathEndingCharacters, hostFinish); + Contract.Assume(pathFinish >= hostFinish || pathFinish < 0); + if (pathFinish < 0) { + this.Path = value.Substring(hostFinish); + } else { + this.Path = value.Substring(hostFinish, pathFinish - hostFinish); + } + } + + this.Path = NormalizePathEscaping(this.Path); + } + + /// <summary> + /// Gets the scheme. + /// </summary> + /// <value>The scheme.</value> + public string Scheme { get; private set; } + + /// <summary> + /// Gets the authority. + /// </summary> + /// <value>The authority.</value> + public string Authority { get; private set; } + + /// <summary> + /// Gets the path of the URI. + /// </summary> + /// <value>The path from the URI.</value> + public string Path { get; private set; } + + /// <summary> + /// Gets the query. + /// </summary> + /// <value>The query.</value> + public string Query { get; private set; } + + /// <summary> + /// Gets the fragment. + /// </summary> + /// <value>The fragment.</value> + public string Fragment { get; private set; } + + /// <summary> + /// Returns a <see cref="System.String"/> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="System.String"/> that represents this instance. + /// </returns> + public override string ToString() { + return this.Scheme + Uri.SchemeDelimiter + this.Authority + this.Path + this.Query + this.Fragment; + } + + /// <summary> + /// Determines whether the specified <see cref="System.Object"/> is equal to this instance. + /// </summary> + /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + SimpleUri other = obj as SimpleUri; + if (other == null) { + return false; + } + + // Note that this equality check is intentionally leaving off the Fragment part + // to match Uri behavior, and is intentionally being case sensitive and insensitive + // for different parts. + return string.Equals(this.Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Authority, other.Authority, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Path, other.Path, StringComparison.Ordinal) && + string.Equals(this.Query, other.Query, StringComparison.Ordinal); + } + + /// <summary> + /// Returns a hash code for this instance. + /// </summary> + /// <returns> + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// </returns> + public override int GetHashCode() { + int hashCode = 0; + hashCode += StringComparer.OrdinalIgnoreCase.GetHashCode(this.Scheme); + hashCode += StringComparer.OrdinalIgnoreCase.GetHashCode(this.Authority); + hashCode += StringComparer.Ordinal.GetHashCode(this.Path); + hashCode += StringComparer.Ordinal.GetHashCode(this.Query); + return hashCode; + } + + /// <summary> + /// Normalizes the characters that are escaped in the given URI path. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <returns>The given path, with exactly those characters escaped which should be.</returns> + private static string NormalizePathEscaping(string path) { + Contract.Requires<ArgumentNullException>(path != null); + + string[] segments = path.Split('/'); + for (int i = 0; i < segments.Length; i++) { + segments[i] = Uri.EscapeDataString(Uri.UnescapeDataString(segments[i])); + } + + return string.Join("/", segments); + } + } + + /// <summary> + /// A URI parser that does not compress paths, such as trimming trailing periods from path segments. + /// </summary> + private class NonPathCompressingUriParser : GenericUriParser { + /// <summary> + /// The field that stores the scheme that this parser is registered under. + /// </summary> + private static FieldInfo schemeField; + + /// <summary> + /// The standard "http" or "https" scheme that this parser is subverting. + /// </summary> + private string standardScheme; + + /// <summary> + /// Initializes a new instance of the <see cref="NonPathCompressingUriParser"/> class. + /// </summary> + /// <param name="standardScheme">The standard scheme that this parser will be subverting.</param> + public NonPathCompressingUriParser(string standardScheme) + : base(GenericUriParserOptions.DontCompressPath | GenericUriParserOptions.IriParsing | GenericUriParserOptions.Idn) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(standardScheme)); + this.standardScheme = standardScheme; + } + + /// <summary> + /// Gets the scheme this parser is registered under. + /// </summary> + /// <value>The registered scheme.</value> + internal string RegisteredScheme { get; private set; } + + /// <summary> + /// Initializes this parser with the actual scheme it should appear to be. + /// </summary> + /// <param name="hideNonStandardScheme">if set to <c>true</c> Uris using this scheme will look like they're using the original standard scheme.</param> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Schemes are traditionally displayed in lowercase.")] + internal void Initialize(bool hideNonStandardScheme) { + if (schemeField == null) { + schemeField = typeof(UriParser).GetField("m_Scheme", BindingFlags.NonPublic | BindingFlags.Instance); + } + + this.RegisteredScheme = (string)schemeField.GetValue(this); + + if (hideNonStandardScheme) { + schemeField.SetValue(this, this.standardScheme.ToLowerInvariant()); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/XriDiscoveryProxyService.cs b/src/DotNetOpenAuth.OpenId/OpenId/XriDiscoveryProxyService.cs new file mode 100644 index 0000000..b1a3430 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/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(DotNetOpenAuthSection.Configuration.OpenId.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, + DotNetOpenAuthSection.Configuration.OpenId.XriResolver.Proxy.Name)); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/OpenId/XriIdentifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/XriIdentifier.cs new file mode 100644 index 0000000..729f603 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/XriIdentifier.cs @@ -0,0 +1,208 @@ +//----------------------------------------------------------------------- +// <copyright file="XriIdentifier.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.Xml; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.RelyingParty; + using DotNetOpenAuth.Xrds; + using DotNetOpenAuth.Yadis; + + /// <summary> + /// An XRI style of OpenID Identifier. + /// </summary> + [Serializable] + [ContractVerification(true)] + [Pure] + public sealed class XriIdentifier : Identifier { + /// <summary> + /// An XRI always starts with one of these symbols. + /// </summary> + internal static readonly char[] GlobalContextSymbols = { '=', '@', '+', '$', '!' }; + + /// <summary> + /// The scheme and separator "xri://" + /// </summary> + private const string XriScheme = "xri://"; + + /// <summary> + /// Backing store for the <see cref="CanonicalXri"/> property. + /// </summary> + private readonly string canonicalXri; + + /// <summary> + /// Initializes a new instance of the <see cref="XriIdentifier"/> class. + /// </summary> + /// <param name="xri">The string value of the XRI.</param> + internal XriIdentifier(string xri) + : this(xri, false) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(xri)); + Contract.Requires<FormatException>(IsValidXri(xri), OpenIdStrings.InvalidXri); + } + + /// <summary> + /// Initializes a new instance of the <see cref="XriIdentifier"/> class. + /// </summary> + /// <param name="xri">The XRI that this Identifier will represent.</param> + /// <param name="requireSsl"> + /// If set to <c>true</c>, discovery and the initial authentication redirect will + /// only succeed if it can be done entirely using SSL. + /// </param> + internal XriIdentifier(string xri, bool requireSsl) + : base(xri, requireSsl) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(xri)); + Contract.Requires<FormatException>(IsValidXri(xri), OpenIdStrings.InvalidXri); + Contract.Assume(xri != null); // Proven by IsValidXri + this.OriginalXri = xri; + this.canonicalXri = CanonicalizeXri(xri); + } + + /// <summary> + /// Gets the original XRI supplied to the constructor. + /// </summary> + internal string OriginalXri { get; private set; } + + /// <summary> + /// Gets the canonical form of the XRI string. + /// </summary> + internal string CanonicalXri { + get { + Contract.Ensures(Contract.Result<string>() != null); + return this.canonicalXri; + } + } + + /// <summary> + /// Tests equality between this XRI and another XRI. + /// </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) { + XriIdentifier other = obj as XriIdentifier; + if (obj != null && other == null && Identifier.EqualityOnStrings) { // test hook to enable MockIdentifier comparison + string objString = obj.ToString(); + ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(objString), "Identifier.ToString() returned a null or empty string."); + other = Identifier.Parse(objString) as XriIdentifier; + } + if (other == null) { + return false; + } + return this.CanonicalXri == other.CanonicalXri; + } + + /// <summary> + /// Returns the hash code of this XRI. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return this.CanonicalXri.GetHashCode(); + } + + /// <summary> + /// Returns the canonical string form of the XRI. + /// </summary> + /// <returns> + /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. + /// </returns> + public override string ToString() { + return this.CanonicalXri; + } + + /// <summary> + /// Tests whether a given string represents a valid XRI format. + /// </summary> + /// <param name="xri">The value to test for XRI validity.</param> + /// <returns> + /// <c>true</c> if the given string constitutes a valid XRI; otherwise, <c>false</c>. + /// </returns> + internal static bool IsValidXri(string xri) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(xri)); + xri = xri.Trim(); + + // TODO: better validation code here + return xri.IndexOfAny(GlobalContextSymbols) == 0 + || xri.StartsWith("(", StringComparison.Ordinal) + || xri.StartsWith(XriScheme, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Returns an <see cref="Identifier"/> that has no URI fragment. + /// Quietly returns the original <see cref="Identifier"/> if it is not + /// a <see cref="UriIdentifier"/> or no fragment exists. + /// </summary> + /// <returns> + /// A new <see cref="Identifier"/> instance if there was a + /// fragment to remove, otherwise this same instance.. + /// </returns> + /// <remarks> + /// XRI Identifiers never have a fragment part, and thus this method + /// always returns this same instance. + /// </remarks> + internal override Identifier TrimFragment() { + return this; + } + + /// <summary> + /// Converts a given identifier to its secure equivalent. + /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS. + /// Discovery is made to require SSL for the entire resolution process. + /// </summary> + /// <param name="secureIdentifier">The newly created secure identifier. + /// If the conversion fails, <paramref name="secureIdentifier"/> retains + /// <i>this</i> identifiers identity, but will never discover any endpoints.</param> + /// <returns> + /// True if the secure conversion was successful. + /// False if the Identifier was originally created with an explicit HTTP scheme. + /// </returns> + [ContractVerification(false)] // bugs/limitations in CC static analysis + internal override bool TryRequireSsl(out Identifier secureIdentifier) { + secureIdentifier = IsDiscoverySecureEndToEnd ? this : new XriIdentifier(this, true); + return true; + } + + /// <summary> + /// Takes any valid form of XRI string and returns the canonical form of the same XRI. + /// </summary> + /// <param name="xri">The xri to canonicalize.</param> + /// <returns>The canonicalized form of the XRI.</returns> + /// <remarks>The canonical form, per the OpenID spec, is no scheme and no whitespace on either end.</remarks> + private static string CanonicalizeXri(string xri) { + Contract.Requires<ArgumentNullException>(xri != null); + Contract.Ensures(Contract.Result<string>() != null); + xri = xri.Trim(); + if (xri.StartsWith(XriScheme, StringComparison.OrdinalIgnoreCase)) { + Contract.Assume(XriScheme.Length <= xri.Length); // should be implied by StartsWith + xri = xri.Substring(XriScheme.Length); + } + return xri; + } + +#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.canonicalXri != null); + } +#endif + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/ServiceElement.cs b/src/DotNetOpenAuth.OpenId/Xrds/ServiceElement.cs new file mode 100644 index 0000000..0acf2b5 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/ServiceElement.cs @@ -0,0 +1,133 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceElement.cs" company="Andrew Arnott, Scott Hanselman"> +// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Xrds { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Xml.XPath; + using DotNetOpenAuth.OpenId; + + /// <summary> + /// The Service element in an XRDS document. + /// </summary> + internal class ServiceElement : XrdsNode, IComparable<ServiceElement> { + /// <summary> + /// Initializes a new instance of the <see cref="ServiceElement"/> class. + /// </summary> + /// <param name="serviceElement">The service element.</param> + /// <param name="parent">The parent.</param> + public ServiceElement(XPathNavigator serviceElement, XrdElement parent) : + base(serviceElement, parent) { + } + + /// <summary> + /// Gets the XRD parent element. + /// </summary> + public XrdElement Xrd { + get { return (XrdElement)ParentNode; } + } + + /// <summary> + /// Gets the priority. + /// </summary> + public int? Priority { + get { + XPathNavigator n = Node.SelectSingleNode("@priority", XmlNamespaceResolver); + return n != null ? n.ValueAsInt : (int?)null; + } + } + + /// <summary> + /// Gets the URI child elements. + /// </summary> + public IEnumerable<UriElement> UriElements { + get { + List<UriElement> uris = new List<UriElement>(); + foreach (XPathNavigator node in Node.Select("xrd:URI", XmlNamespaceResolver)) { + uris.Add(new UriElement(node, this)); + } + uris.Sort(); + return uris; + } + } + + /// <summary> + /// Gets the type child elements. + /// </summary> + /// <value>The type elements.</value> + public IEnumerable<TypeElement> TypeElements { + get { + foreach (XPathNavigator node in Node.Select("xrd:Type", XmlNamespaceResolver)) { + yield return new TypeElement(node, this); + } + } + } + + /// <summary> + /// Gets the type child element's URIs. + /// </summary> + public string[] TypeElementUris { + get { + return this.TypeElements.Select(type => type.Uri).ToArray(); + } + } + + /// <summary> + /// Gets the OP Local Identifier. + /// </summary> + public Identifier ProviderLocalIdentifier { + get { + var n = Node.SelectSingleNode("xrd:LocalID", XmlNamespaceResolver) + ?? Node.SelectSingleNode("openid10:Delegate", XmlNamespaceResolver); + if (n != null && n.Value != null) { + string value = n.Value.Trim(); + if (value.Length > 0) { + return n.Value; + } + } + + return null; + } + } + + #region IComparable<ServiceElement> Members + + /// <summary> + /// Compares the current object with another object of the same type. + /// </summary> + /// <param name="other">An object to compare with this object.</param> + /// <returns> + /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings: + /// Value + /// Meaning + /// Less than zero + /// This object is less than the <paramref name="other"/> parameter. + /// Zero + /// This object is equal to <paramref name="other"/>. + /// Greater than zero + /// This object is greater than <paramref name="other"/>. + /// </returns> + public int CompareTo(ServiceElement other) { + if (other == null) { + return -1; + } + if (this.Priority.HasValue && other.Priority.HasValue) { + return this.Priority.Value.CompareTo(other.Priority.Value); + } else { + if (this.Priority.HasValue) { + return -1; + } else if (other.Priority.HasValue) { + return 1; + } else { + return 0; + } + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/TypeElement.cs b/src/DotNetOpenAuth.OpenId/Xrds/TypeElement.cs new file mode 100644 index 0000000..c413629 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/TypeElement.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="TypeElement.cs" company="Andrew Arnott, Scott Hanselman"> +// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Xrds { + using System; + using System.Diagnostics.Contracts; + using System.Xml.XPath; + + /// <summary> + /// The Type element in an XRDS document. + /// </summary> + internal class TypeElement : XrdsNode { + /// <summary> + /// Initializes a new instance of the <see cref="TypeElement"/> class. + /// </summary> + /// <param name="typeElement">The type element.</param> + /// <param name="parent">The parent.</param> + public TypeElement(XPathNavigator typeElement, ServiceElement parent) : + base(typeElement, parent) { + Contract.Requires<ArgumentNullException>(typeElement != null); + Contract.Requires<ArgumentNullException>(parent != null); + } + + /// <summary> + /// Gets the URI. + /// </summary> + public string Uri { + get { return Node.Value; } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/UriElement.cs b/src/DotNetOpenAuth.OpenId/Xrds/UriElement.cs new file mode 100644 index 0000000..a67d259 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/UriElement.cs @@ -0,0 +1,98 @@ +//----------------------------------------------------------------------- +// <copyright file="UriElement.cs" company="Andrew Arnott, Scott Hanselman"> +// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Xrds { + using System; + using System.Xml.XPath; + + /// <summary> + /// The Uri element in an XRDS document. + /// </summary> + internal class UriElement : XrdsNode, IComparable<UriElement> { + /// <summary> + /// Initializes a new instance of the <see cref="UriElement"/> class. + /// </summary> + /// <param name="uriElement">The URI element.</param> + /// <param name="service">The service.</param> + public UriElement(XPathNavigator uriElement, ServiceElement service) : + base(uriElement, service) { + } + + /// <summary> + /// Gets the priority. + /// </summary> + public int? Priority { + get { + XPathNavigator n = Node.SelectSingleNode("@priority", XmlNamespaceResolver); + return n != null ? n.ValueAsInt : (int?)null; + } + } + + /// <summary> + /// Gets the URI. + /// </summary> + public Uri Uri { + get { + if (Node.Value != null) { + string value = Node.Value.Trim(); + if (value.Length > 0) { + return new Uri(value); + } + } + + return null; + } + } + + /// <summary> + /// Gets the parent service. + /// </summary> + public ServiceElement Service { + get { return (ServiceElement)ParentNode; } + } + + #region IComparable<UriElement> Members + + /// <summary> + /// Compares the current object with another object of the same type. + /// </summary> + /// <param name="other">An object to compare with this object.</param> + /// <returns> + /// A 32-bit signed integer that indicates the relative order of the objects being compared. The return value has the following meanings: + /// Value + /// Meaning + /// Less than zero + /// This object is less than the <paramref name="other"/> parameter. + /// Zero + /// This object is equal to <paramref name="other"/>. + /// Greater than zero + /// This object is greater than <paramref name="other"/>. + /// </returns> + public int CompareTo(UriElement other) { + if (other == null) { + return -1; + } + int compare = this.Service.CompareTo(other.Service); + if (compare != 0) { + return compare; + } + + if (this.Priority.HasValue && other.Priority.HasValue) { + return this.Priority.Value.CompareTo(other.Priority.Value); + } else { + if (this.Priority.HasValue) { + return -1; + } else if (other.Priority.HasValue) { + return 1; + } else { + return 0; + } + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/XrdElement.cs b/src/DotNetOpenAuth.OpenId/Xrds/XrdElement.cs new file mode 100644 index 0000000..2cdc720 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/XrdElement.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------- +// <copyright file="XrdElement.cs" company="Andrew Arnott, Scott Hanselman"> +// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Xrds { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Xml.XPath; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + + /// <summary> + /// The Xrd element in an XRDS document. + /// </summary> + internal class XrdElement : XrdsNode { + /// <summary> + /// Initializes a new instance of the <see cref="XrdElement"/> class. + /// </summary> + /// <param name="xrdElement">The XRD element.</param> + /// <param name="parent">The parent.</param> + public XrdElement(XPathNavigator xrdElement, XrdsDocument parent) : + base(xrdElement, parent) { + } + + /// <summary> + /// Gets the child service elements. + /// </summary> + /// <value>The services.</value> + public IEnumerable<ServiceElement> Services { + get { + // We should enumerate them in priority order + List<ServiceElement> services = new List<ServiceElement>(); + foreach (XPathNavigator node in Node.Select("xrd:Service", XmlNamespaceResolver)) { + services.Add(new ServiceElement(node, this)); + } + services.Sort(); + return services; + } + } + + /// <summary> + /// Gets a value indicating whether this XRD element's resolution at the XRI resolver was successful. + /// </summary> + /// <value> + /// <c>true</c> if this XRD's resolution was successful; otherwise, <c>false</c>. + /// </value> + public bool IsXriResolutionSuccessful { + get { + return this.XriResolutionStatusCode == 100; + } + } + + /// <summary> + /// Gets the canonical ID (i-number) for this element. + /// </summary> + public string CanonicalID { + get { + var n = Node.SelectSingleNode("xrd:CanonicalID", XmlNamespaceResolver); + return n != null ? n.Value : null; + } + } + + /// <summary> + /// Gets a value indicating whether the <see cref="CanonicalID"/> was verified. + /// </summary> + public bool IsCanonicalIdVerified { + get { + var n = Node.SelectSingleNode("xrd:Status", XmlNamespaceResolver); + return n != null && string.Equals(n.GetAttribute("cid", string.Empty), "verified", StringComparison.Ordinal); + } + } + + /// <summary> + /// Gets the services for OP Identifiers. + /// </summary> + public IEnumerable<ServiceElement> OpenIdProviderIdentifierServices { + get { return this.SearchForServiceTypeUris(p => p.OPIdentifierServiceTypeURI); } + } + + /// <summary> + /// Gets the services for Claimed Identifiers. + /// </summary> + public IEnumerable<ServiceElement> OpenIdClaimedIdentifierServices { + get { return this.SearchForServiceTypeUris(p => p.ClaimedIdentifierServiceTypeURI); } + } + + /// <summary> + /// Gets the services that would be discoverable at an RP for return_to verification. + /// </summary> + public IEnumerable<ServiceElement> OpenIdRelyingPartyReturnToServices { + get { return this.SearchForServiceTypeUris(p => p.RPReturnToTypeURI); } + } + + /// <summary> + /// Gets the services that would be discoverable at an RP for the UI extension icon. + /// </summary> + public IEnumerable<ServiceElement> OpenIdRelyingPartyIcons { + get { return this.SearchForServiceTypeUris(p => "http://specs.openid.net/extensions/ui/icon"); } + } + + /// <summary> + /// Gets an enumeration of all Service/URI elements, sorted in priority order. + /// </summary> + public IEnumerable<UriElement> ServiceUris { + get { + return from service in this.Services + from uri in service.UriElements + select uri; + } + } + + /// <summary> + /// Gets the XRI resolution status code. + /// </summary> + private int XriResolutionStatusCode { + get { + var n = Node.SelectSingleNode("xrd:Status", XmlNamespaceResolver); + string codeString = null; + ErrorUtilities.VerifyProtocol(n != null && !string.IsNullOrEmpty(codeString = n.GetAttribute("code", string.Empty)), XrdsStrings.XriResolutionStatusMissing); + int code; + ErrorUtilities.VerifyProtocol(int.TryParse(codeString, out code) && code >= 100 && code < 400, XrdsStrings.XriResolutionStatusMissing); + return code; + } + } + + /// <summary> + /// Searches for service sub-elements that have Type URI sub-elements that match + /// one that we have for a known OpenID protocol version. + /// </summary> + /// <param name="p">A function that selects what element of the OpenID Protocol we're interested in finding.</param> + /// <returns>A sequence of service elements that match the search criteria, sorted in XRDS @priority attribute order.</returns> + internal IEnumerable<ServiceElement> SearchForServiceTypeUris(Func<Protocol, string> p) { + var xpath = new StringBuilder(); + xpath.Append("xrd:Service["); + foreach (var protocol in Protocol.AllVersions) { + string typeUri = p(protocol); + if (typeUri == null) { + continue; + } + xpath.Append("xrd:Type/text()='"); + xpath.Append(typeUri); + xpath.Append("' or "); + } + xpath.Length -= 4; + xpath.Append("]"); + var services = new List<ServiceElement>(); + foreach (XPathNavigator service in Node.Select(xpath.ToString(), XmlNamespaceResolver)) { + services.Add(new ServiceElement(service, this)); + } + + // Put the services in their own defined priority order + services.Sort(); + return services; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/XrdsDocument.cs b/src/DotNetOpenAuth.OpenId/Xrds/XrdsDocument.cs new file mode 100644 index 0000000..040c994 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/XrdsDocument.cs @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------- +// <copyright file="XrdsDocument.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Xrds { + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Xml; + using System.Xml.XPath; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + + /// <summary> + /// An XRDS document. + /// </summary> + internal class XrdsDocument : XrdsNode { + /// <summary> + /// The namespace used by XML digital signatures. + /// </summary> + private const string XmlDSigNamespace = "http://www.w3.org/2000/09/xmldsig#"; + + /// <summary> + /// The namespace used by Google Apps for Domains for OpenID URI templates. + /// </summary> + private const string GoogleOpenIdNamespace = "http://namespace.google.com/openid/xmlns"; + + /// <summary> + /// Initializes a new instance of the <see cref="XrdsDocument"/> class. + /// </summary> + /// <param name="xrdsNavigator">The root node of the XRDS document.</param> + public XrdsDocument(XPathNavigator xrdsNavigator) + : base(xrdsNavigator) { + XmlNamespaceResolver.AddNamespace("xrd", XrdsNode.XrdNamespace); + XmlNamespaceResolver.AddNamespace("xrds", XrdsNode.XrdsNamespace); + XmlNamespaceResolver.AddNamespace("openid10", Protocol.V10.XmlNamespace); + XmlNamespaceResolver.AddNamespace("ds", XmlDSigNamespace); + XmlNamespaceResolver.AddNamespace("google", GoogleOpenIdNamespace); + } + + /// <summary> + /// Initializes a new instance of the <see cref="XrdsDocument"/> class. + /// </summary> + /// <param name="reader">The Xml reader positioned at the root node of the XRDS document.</param> + public XrdsDocument(XmlReader reader) + : this(new XPathDocument(reader).CreateNavigator()) { } + + /// <summary> + /// Initializes a new instance of the <see cref="XrdsDocument"/> class. + /// </summary> + /// <param name="xml">The text that is the XRDS document.</param> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Fixing would decrease readability, and not likely avoid any finalizer on a StringReader anyway.")] + public XrdsDocument(string xml) + : this(new XPathDocument(new StringReader(xml)).CreateNavigator()) { } + + /// <summary> + /// Gets the XRD child elements of the document. + /// </summary> + public IEnumerable<XrdElement> XrdElements { + get { + // We may be looking at a full XRDS document (in the case of YADIS discovery) + // or we may be looking at just an individual XRD element from a larger document + // if we asked xri.net for just one. + if (Node.SelectSingleNode("/xrds:XRDS", XmlNamespaceResolver) != null) { + foreach (XPathNavigator node in Node.Select("/xrds:XRDS/xrd:XRD", XmlNamespaceResolver)) { + yield return new XrdElement(node, this); + } + } else { + XPathNavigator node = Node.SelectSingleNode("/xrd:XRD", XmlNamespaceResolver); + if (node != null) { + yield return new XrdElement(node, this); + } + } + } + } + + /// <summary> + /// Gets a value indicating whether all child XRD elements were resolved successfully. + /// </summary> + internal bool IsXrdResolutionSuccessful { + get { return this.XrdElements.All(xrd => xrd.IsXriResolutionSuccessful); } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/XrdsNode.cs b/src/DotNetOpenAuth.OpenId/Xrds/XrdsNode.cs new file mode 100644 index 0000000..39bd9b9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/XrdsNode.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="XrdsNode.cs" company="Andrew Arnott, Scott Hanselman"> +// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Xrds { + using System; + using System.Diagnostics.Contracts; + using System.Xml; + using System.Xml.XPath; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A node in an XRDS document. + /// </summary> + internal class XrdsNode { + /// <summary> + /// The XRD namespace xri://$xrd*($v*2.0) + /// </summary> + internal const string XrdNamespace = "xri://$xrd*($v*2.0)"; + + /// <summary> + /// The XRDS namespace xri://$xrds + /// </summary> + internal const string XrdsNamespace = "xri://$xrds"; + + /// <summary> + /// Initializes a new instance of the <see cref="XrdsNode"/> class. + /// </summary> + /// <param name="node">The node represented by this instance.</param> + /// <param name="parentNode">The parent node.</param> + protected XrdsNode(XPathNavigator node, XrdsNode parentNode) { + Contract.Requires<ArgumentNullException>(node != null); + Contract.Requires<ArgumentNullException>(parentNode != null); + + this.Node = node; + this.ParentNode = parentNode; + this.XmlNamespaceResolver = this.ParentNode.XmlNamespaceResolver; + } + + /// <summary> + /// Initializes a new instance of the <see cref="XrdsNode"/> class. + /// </summary> + /// <param name="document">The document's root node, which this instance represents.</param> + protected XrdsNode(XPathNavigator document) { + Contract.Requires<ArgumentNullException>(document != null); + Contract.Requires<ArgumentException>(document.NameTable != null); + + this.Node = document; + this.XmlNamespaceResolver = new XmlNamespaceManager(document.NameTable); + } + + /// <summary> + /// Gets the node. + /// </summary> + internal XPathNavigator Node { get; private set; } + + /// <summary> + /// Gets the parent node, or null if this is the root node. + /// </summary> + protected internal XrdsNode ParentNode { get; private set; } + + /// <summary> + /// Gets the XML namespace resolver to use in XPath expressions. + /// </summary> + protected internal XmlNamespaceManager XmlNamespaceResolver { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.Designer.cs b/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.Designer.cs new file mode 100644 index 0000000..2279b5f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30104.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.Xrds { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class XrdsStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal XrdsStrings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.Xrds.XrdsStrings", typeof(XrdsStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to XRI CanonicalID verification failed.. + /// </summary> + internal static string CIDVerificationFailed { + get { + return ResourceManager.GetString("CIDVerificationFailed", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failure parsing XRDS document.. + /// </summary> + internal static string InvalidXRDSDocument { + get { + return ResourceManager.GetString("InvalidXRDSDocument", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The XRDS document for XRI {0} is missing the required CanonicalID element.. + /// </summary> + internal static string MissingCanonicalIDElement { + get { + return ResourceManager.GetString("MissingCanonicalIDElement", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Could not find XRI resolution Status tag or code attribute was invalid.. + /// </summary> + internal static string XriResolutionStatusMissing { + get { + return ResourceManager.GetString("XriResolutionStatusMissing", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.resx b/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.resx new file mode 100644 index 0000000..acb43f2 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="CIDVerificationFailed" xml:space="preserve"> + <value>XRI CanonicalID verification failed.</value> + </data> + <data name="InvalidXRDSDocument" xml:space="preserve"> + <value>Failure parsing XRDS document.</value> + </data> + <data name="MissingCanonicalIDElement" xml:space="preserve"> + <value>The XRDS document for XRI {0} is missing the required CanonicalID element.</value> + </data> + <data name="XriResolutionStatusMissing" xml:space="preserve"> + <value>Could not find XRI resolution Status tag or code attribute was invalid.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.sr.resx b/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.sr.resx new file mode 100644 index 0000000..8e2d09a --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Xrds/XrdsStrings.sr.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="CIDVerificationFailed" xml:space="preserve"> + <value>XRI CanonicalID provera neuspešna.</value> + </data> + <data name="InvalidXRDSDocument" xml:space="preserve"> + <value>Greška u obradi XRDS dokumenta.</value> + </data> + <data name="MissingCanonicalIDElement" xml:space="preserve"> + <value>XRDS dokumentu za XRI {0} nedostaje neophodni CanonicalID element.</value> + </data> + <data name="XriResolutionStatusMissing" xml:space="preserve"> + <value>Ne može se pronaći XRI resolution Status tag ili je code attribute neispravan.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OpenId/XrdsPublisher.cs b/src/DotNetOpenAuth.OpenId/XrdsPublisher.cs new file mode 100644 index 0000000..03c32c1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/XrdsPublisher.cs @@ -0,0 +1,229 @@ +//----------------------------------------------------------------------- +// <copyright file="XrdsPublisher.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Drawing.Design; + using System.Text; + using System.Web; + using System.Web.UI; + using System.Web.UI.WebControls; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Yadis; + + /// <summary> + /// The locations the YADIS protocol describes can contain a reference + /// to an XRDS document. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Flags] + public enum XrdsUrlLocations { + /// <summary> + /// The XRDS document should not be advertised anywhere. + /// </summary> + /// <remarks> + /// When the XRDS document is not referenced from anywhere, + /// the XRDS content is only available when + /// <see cref="XrdsPublisher.XrdsAutoAnswer"/> is <c>true</c> + /// and the discovering client includes an + /// "Accept: application/xrds+xml" HTTP header. + /// </remarks> + None = 0x0, + + /// <summary> + /// Indicates XRDS document referencing from an HTTP protocol header (outside the HTML). + /// </summary> + HttpHeader = 0x1, + + /// <summary> + /// Indicates XRDS document referencing from within an HTML page's <HEAD> tag. + /// </summary> + HtmlMeta = 0x2, + + /// <summary> + /// Indicates XRDS document referencing in both HTTP headers and HTML HEAD tags. + /// </summary> + Both = 0x3, + } + + /// <summary> + /// An ASP.NET control that advertises an XRDS document and even responds to specially + /// crafted requests to retrieve it. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [DefaultProperty("XrdsLocation")] + [ToolboxData("<{0}:XrdsPublisher runat=server></{0}:XrdsPublisher>")] + public class XrdsPublisher : Control { + /// <summary> + /// The view state key to ues for storing the value of the <see cref="XrdsUrl"/> property. + /// </summary> + private const string XrdsUrlViewStateKey = "XrdsUrl"; + + /// <summary> + /// The default value for the <see cref="XrdsAdvertisement"/> property. + /// </summary> + private const XrdsUrlLocations XrdsAdvertisementDefault = XrdsUrlLocations.HttpHeader; + + /// <summary> + /// The view state key to ues for storing the value of the <see cref="XrdsAdvertisement"/> property. + /// </summary> + private const string XrdsAdvertisementViewStateKey = "XrdsAdvertisement"; + + /// <summary> + /// The default value for the <see cref="XrdsAutoAnswer"/> property. + /// </summary> + private const bool XrdsAutoAnswerDefault = true; + + /// <summary> + /// The view state key to ues for storing the value of the <see cref="XrdsAutoAnswer"/> property. + /// </summary> + private const string XrdsAutoAnswerViewStateKey = "XrdsAutoAnswer"; + + /// <summary> + /// The default value for the <see cref="Enabled"/> property. + /// </summary> + private const bool EnabledDefault = true; + + /// <summary> + /// The view state key to ues for storing the value of the <see cref="Enabled"/> property. + /// </summary> + private const string EnabledViewStateKey = "Enabled"; + + /// <summary> + /// Initializes a new instance of the <see cref="XrdsPublisher"/> class. + /// </summary> + public XrdsPublisher() { + Reporting.RecordFeatureUse(this); + } + + #region Properties + + /// <summary> + /// Gets or sets the location of the XRDS document. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid")] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Category("Behavior"), Bindable(true)] + [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] + public string XrdsUrl { + get { + return (string)ViewState[XrdsUrlViewStateKey]; + } + + set { + UriUtil.ValidateResolvableUrl(Page, DesignMode, value); + ViewState[XrdsUrlViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets where the XRDS document URL is advertised in the web response. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Category("Behavior"), DefaultValue(XrdsAdvertisementDefault), Bindable(true)] + [Description("Where the XRDS document URL is advertised in the web response.")] + public XrdsUrlLocations XrdsAdvertisement { + get { + return ViewState[XrdsAdvertisementViewStateKey] == null ? + XrdsAdvertisementDefault : (XrdsUrlLocations)ViewState[XrdsAdvertisementViewStateKey]; + } + + set { + ViewState[XrdsAdvertisementViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether a specially crafted YADIS + /// search for an XRDS document is immediately answered by this control. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Correct spelling")] + [Category("Behavior"), DefaultValue(XrdsAutoAnswerDefault), Bindable(true)] + [Description("Whether XRDS requests should be immediately answered with the XRDS document if it is served by this web application.")] + public bool XrdsAutoAnswer { + get { + return ViewState[XrdsAutoAnswerViewStateKey] == null ? + XrdsAutoAnswerDefault : (bool)ViewState[XrdsAutoAnswerViewStateKey]; + } + + set { + ViewState[XrdsAutoAnswerViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether the XRDS document is advertised. + /// </summary> + [Category("Behavior"), DefaultValue(EnabledDefault)] + public bool Enabled { + get { + return ViewState[EnabledViewStateKey] == null ? + EnabledDefault : (bool)ViewState[EnabledViewStateKey]; + } + + set { + ViewState[EnabledViewStateKey] = value; + } + } + + #endregion + + /// <summary> + /// Detects YADIS requests for the XRDS document and responds immediately + /// if <see cref="XrdsAutoAnswer"/> is true. + /// </summary> + /// <param name="e">The <see cref="T:System.EventArgs"/> object that contains the event data.</param> + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + + if (!this.Enabled) { + return; + } + + if (!this.Page.IsPostBack) { + if (this.XrdsAutoAnswer && !string.IsNullOrEmpty(this.XrdsUrl) && + this.XrdsUrl.StartsWith("~/", StringComparison.Ordinal)) { + // Check for the presence of an accept types header that is looking + // for the XRDS document specifically. + if (this.Page.Request.AcceptTypes != null && Array.IndexOf(this.Page.Request.AcceptTypes, ContentTypes.Xrds) >= 0) { + // Respond to the caller immediately with an XRDS document + // and avoid sending the whole web page's contents to the + // client since it isn't interested anyway. + // We do NOT simply send a 301 redirect here because that would + // alter the Claimed Identifier. + this.Page.Server.Transfer(this.XrdsUrl); + } + } + } + } + + /// <summary> + /// Renders the HTTP Header and/or HTML HEAD tags. + /// </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.Diagnostics.Contracts.__ContractsRuntime.Assume(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] + protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); + if (this.Enabled && this.Visible && !string.IsNullOrEmpty(this.XrdsUrl)) { + Uri xrdsAddress = new Uri(MessagingUtilities.GetRequestUrlFromContext(), Page.Response.ApplyAppPathModifier(this.XrdsUrl)); + if ((this.XrdsAdvertisement & XrdsUrlLocations.HttpHeader) != 0) { + Page.Response.AddHeader(Yadis.Yadis.HeaderName, xrdsAddress.AbsoluteUri); + } + if ((this.XrdsAdvertisement & XrdsUrlLocations.HtmlMeta) != 0) { + writer.WriteBeginTag("meta"); + writer.WriteAttribute("http-equiv", Yadis.Yadis.HeaderName); + writer.WriteAttribute("content", xrdsAddress.AbsoluteUri); + writer.Write("/>"); + writer.WriteLine(); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Yadis/ContentTypes.cs b/src/DotNetOpenAuth.OpenId/Yadis/ContentTypes.cs new file mode 100644 index 0000000..30745ee --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Yadis/ContentTypes.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// <copyright file="ContentTypes.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Yadis { + /// <summary> + /// String constants for various content-type header values used in YADIS discovery. + /// </summary> + internal static class ContentTypes { + /// <summary> + /// The text/html content-type + /// </summary> + public const string Html = "text/html"; + + /// <summary> + /// The application/xhtml+xml content-type + /// </summary> + public const string XHtml = "application/xhtml+xml"; + + /// <summary> + /// The application/xrds+xml content-type + /// </summary> + public const string Xrds = "application/xrds+xml"; + + /// <summary> + /// The text/xml content type + /// </summary> + public const string Xml = "text/xml"; + } +} diff --git a/src/DotNetOpenAuth.OpenId/Yadis/DiscoveryResult.cs b/src/DotNetOpenAuth.OpenId/Yadis/DiscoveryResult.cs new file mode 100644 index 0000000..06c6fc7 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Yadis/DiscoveryResult.cs @@ -0,0 +1,106 @@ +//----------------------------------------------------------------------- +// <copyright file="DiscoveryResult.cs" company="Scott Hanselman, Andrew Arnott"> +// Copyright (c) Scott Hanselman, Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Yadis { + using System; + using System.IO; + using System.Net.Mime; + using System.Web.UI.HtmlControls; + using System.Xml; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Contains the result of YADIS discovery. + /// </summary> + internal class DiscoveryResult { + /// <summary> + /// The original web response, backed up here if the final web response is the preferred response to use + /// in case it turns out to not work out. + /// </summary> + private CachedDirectWebResponse htmlFallback; + + /// <summary> + /// Initializes a new instance of the <see cref="DiscoveryResult"/> class. + /// </summary> + /// <param name="requestUri">The user-supplied identifier.</param> + /// <param name="initialResponse">The initial response.</param> + /// <param name="finalResponse">The final response.</param> + public DiscoveryResult(Uri requestUri, CachedDirectWebResponse initialResponse, CachedDirectWebResponse finalResponse) { + this.RequestUri = requestUri; + this.NormalizedUri = initialResponse.FinalUri; + if (finalResponse == null || finalResponse.Status != System.Net.HttpStatusCode.OK) { + this.ApplyHtmlResponse(initialResponse); + } else { + this.ContentType = finalResponse.ContentType; + this.ResponseText = finalResponse.GetResponseString(); + this.IsXrds = true; + if (initialResponse != finalResponse) { + this.YadisLocation = finalResponse.RequestUri; + } + + // Back up the initial HTML response in case the XRDS is not useful. + this.htmlFallback = initialResponse; + } + } + + /// <summary> + /// Gets the URI of the original YADIS discovery request. + /// This is the user supplied Identifier as given in the original + /// YADIS discovery request. + /// </summary> + public Uri RequestUri { get; private set; } + + /// <summary> + /// Gets the fully resolved (after redirects) URL of the user supplied Identifier. + /// This becomes the ClaimedIdentifier. + /// </summary> + public Uri NormalizedUri { get; private set; } + + /// <summary> + /// Gets the location the XRDS document was downloaded from, if different + /// from the user supplied Identifier. + /// </summary> + public Uri YadisLocation { get; private set; } + + /// <summary> + /// Gets the Content-Type associated with the <see cref="ResponseText"/>. + /// </summary> + public ContentType ContentType { get; private set; } + + /// <summary> + /// Gets the text in the final response. + /// This may be an XRDS document or it may be an HTML document, + /// as determined by the <see cref="IsXrds"/> property. + /// </summary> + public string ResponseText { get; private set; } + + /// <summary> + /// Gets a value indicating whether the <see cref="ResponseText"/> + /// represents an XRDS document. False if the response is an HTML document. + /// </summary> + public bool IsXrds { get; private set; } + + /// <summary> + /// Reverts to the HTML response after the XRDS response didn't work out. + /// </summary> + internal void TryRevertToHtmlResponse() { + if (this.htmlFallback != null) { + this.ApplyHtmlResponse(this.htmlFallback); + this.htmlFallback = null; + } + } + + /// <summary> + /// Applies the HTML response to the object. + /// </summary> + /// <param name="initialResponse">The initial response.</param> + private void ApplyHtmlResponse(CachedDirectWebResponse initialResponse) { + this.ContentType = initialResponse.ContentType; + this.ResponseText = initialResponse.GetResponseString(); + this.IsXrds = this.ContentType != null && this.ContentType.MediaType == ContentTypes.Xrds; + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Yadis/HtmlParser.cs b/src/DotNetOpenAuth.OpenId/Yadis/HtmlParser.cs new file mode 100644 index 0000000..a6b64c1 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Yadis/HtmlParser.cs @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------- +// <copyright file="HtmlParser.cs" company="Andrew Arnott, Scott Hanselman, Jason Alexander"> +// Copyright (c) Andrew Arnott, Scott Hanselman, Jason Alexander. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Yadis { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Web; + using System.Web.UI.HtmlControls; + + /// <summary> + /// An HTML HEAD tag parser. + /// </summary> + internal static class HtmlParser { + /// <summary> + /// Common flags to use on regex tests. + /// </summary> + private const RegexOptions Flags = RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.Compiled | RegexOptions.IgnoreCase; + + /// <summary> + /// A regular expression designed to select tags (?) + /// </summary> + private const string TagExpr = "\n# Starts with the tag name at a word boundary, where the tag name is\n# not a namespace\n<{0}\\b(?!:)\n \n# All of the stuff up to a \">\", hopefully attributes.\n(?<attrs>[^>]*?)\n \n(?: # Match a short tag\n />\n \n| # Match a full tag\n >\n \n (?<contents>.*?)\n \n # Closed by\n (?: # One of the specified close tags\n </?{1}\\s*>\n \n # End of the string\n | \\Z\n \n )\n \n)\n "; + + /// <summary> + /// A regular expression designed to select start tags (?) + /// </summary> + private const string StartTagExpr = "\n# Starts with the tag name at a word boundary, where the tag name is\n# not a namespace\n<{0}\\b(?!:)\n \n# All of the stuff up to a \">\", hopefully attributes.\n(?<attrs>[^>]*?)\n \n(?: # Match a short tag\n />\n \n| # Match a full tag\n >\n )\n "; + + /// <summary> + /// A regular expression designed to select attributes within a tag. + /// </summary> + private static readonly Regex attrRe = new Regex("\n# Must start with a sequence of word-characters, followed by an equals sign\n(?<attrname>(\\w|-)+)=\n\n# Then either a quoted or unquoted attribute\n(?:\n\n # Match everything that's between matching quote marks\n (?<qopen>[\"\\'])(?<attrval>.*?)\\k<qopen>\n|\n\n # If the value is not quoted, match up to whitespace\n (?<attrval>(?:[^\\s<>/]|/(?!>))+)\n)\n\n|\n\n(?<endtag>[<>])\n ", Flags); + + /// <summary> + /// A regular expression designed to select the HEAD tag. + /// </summary> + private static readonly Regex headRe = TagMatcher("head", new[] { "body" }); + + /// <summary> + /// A regular expression designed to select the HTML tag. + /// </summary> + private static readonly Regex htmlRe = TagMatcher("html", new string[0]); + + /// <summary> + /// A regular expression designed to remove all comments and scripts from a string. + /// </summary> + private static readonly Regex removedRe = new Regex(@"<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b[^>]*>.*?</script>", Flags); + + /// <summary> + /// Finds all the HTML HEAD tag child elements that match the tag name of a given type. + /// </summary> + /// <typeparam name="T">The HTML tag of interest.</typeparam> + /// <param name="html">The HTML to scan.</param> + /// <returns>A sequence of the matching elements.</returns> + public static IEnumerable<T> HeadTags<T>(string html) where T : HtmlControl, new() { + html = removedRe.Replace(html, string.Empty); + Match match = htmlRe.Match(html); + string tagName = (new T()).TagName; + if (match.Success) { + Match match2 = headRe.Match(html, match.Index, match.Length); + if (match2.Success) { + string text = null; + string text2 = null; + Regex regex = StartTagMatcher(tagName); + for (Match match3 = regex.Match(html, match2.Index, match2.Length); match3.Success; match3 = match3.NextMatch()) { + int beginning = (match3.Index + tagName.Length) + 1; + int length = (match3.Index + match3.Length) - beginning; + Match match4 = attrRe.Match(html, beginning, length); + var headTag = new T(); + while (match4.Success) { + if (match4.Groups["endtag"].Success) { + break; + } + text = match4.Groups["attrname"].Value; + text2 = HttpUtility.HtmlDecode(match4.Groups["attrval"].Value); + headTag.Attributes.Add(text, text2); + match4 = match4.NextMatch(); + } + yield return headTag; + } + } + } + } + + /// <summary> + /// Filters a list of controls based on presence of an attribute. + /// </summary> + /// <typeparam name="T">The type of HTML controls being filtered.</typeparam> + /// <param name="sequence">The sequence.</param> + /// <param name="attribute">The attribute.</param> + /// <returns>A filtered sequence of attributes.</returns> + internal static IEnumerable<T> WithAttribute<T>(this IEnumerable<T> sequence, string attribute) where T : HtmlControl { + Contract.Requires<ArgumentNullException>(sequence != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(attribute)); + return sequence.Where(tag => tag.Attributes[attribute] != null); + } + + /// <summary> + /// Generates a regular expression that will find a given HTML tag. + /// </summary> + /// <param name="tagName">Name of the tag.</param> + /// <param name="closeTags">The close tags (?).</param> + /// <returns>The created regular expression.</returns> + private static Regex TagMatcher(string tagName, params string[] closeTags) { + string text2; + if (closeTags.Length > 0) { + StringBuilder builder = new StringBuilder(); + builder.AppendFormat("(?:{0}", tagName); + int index = 0; + string[] textArray = closeTags; + int length = textArray.Length; + while (index < length) { + string text = textArray[index]; + index++; + builder.AppendFormat("|{0}", text); + } + builder.Append(")"); + text2 = builder.ToString(); + } else { + text2 = tagName; + } + return new Regex(string.Format(CultureInfo.InvariantCulture, TagExpr, tagName, text2), Flags); + } + + /// <summary> + /// Generates a regular expression designed to find a given tag. + /// </summary> + /// <param name="tagName">The tag to find.</param> + /// <returns>The created regular expression.</returns> + private static Regex StartTagMatcher(string tagName) { + return new Regex(string.Format(CultureInfo.InvariantCulture, StartTagExpr, tagName), Flags); + } + } +} diff --git a/src/DotNetOpenAuth.OpenId/Yadis/Yadis.cs b/src/DotNetOpenAuth.OpenId/Yadis/Yadis.cs new file mode 100644 index 0000000..357dd8d --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/Yadis/Yadis.cs @@ -0,0 +1,205 @@ +//----------------------------------------------------------------------- +// <copyright file="Yadis.cs" company="Andrew Arnott, Scott Hanselman"> +// Copyright (c) Andrew Arnott, Scott Hanselman. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Yadis { + using System; + using System.Diagnostics.Contracts; + using System.IO; + using System.Net; + using System.Net.Cache; + using System.Web.UI.HtmlControls; + using System.Xml; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.Xrds; + + /// <summary> + /// YADIS discovery manager. + /// </summary> + internal class Yadis { + /// <summary> + /// The HTTP header to look for in responses to declare where the XRDS document should be found. + /// </summary> + internal const string HeaderName = "X-XRDS-Location"; + + /// <summary> + /// Gets or sets the cache that can be used for HTTP requests made during identifier discovery. + /// </summary> +#if DEBUG + internal static readonly RequestCachePolicy IdentifierDiscoveryCachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.BypassCache); +#else + internal static readonly RequestCachePolicy IdentifierDiscoveryCachePolicy = new HttpRequestCachePolicy(DotNetOpenAuthSection.Configuration.OpenId.CacheDiscovery ? HttpRequestCacheLevel.CacheIfAvailable : HttpRequestCacheLevel.BypassCache); +#endif + + /// <summary> + /// The maximum number of bytes to read from an HTTP response + /// in searching for a link to a YADIS document. + /// </summary> + internal const int MaximumResultToScan = 1024 * 1024; + + /// <summary> + /// Performs YADIS discovery on some identifier. + /// </summary> + /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param> + /// <param name="uri">The URI to perform discovery on.</param> + /// <param name="requireSsl">Whether discovery should fail if any step of it is not encrypted.</param> + /// <returns> + /// The result of discovery on the given URL. + /// Null may be returned if an error occurs, + /// or if <paramref name="requireSsl"/> is true but part of discovery + /// is not protected by SSL. + /// </returns> + public static DiscoveryResult Discover(IDirectWebRequestHandler requestHandler, UriIdentifier uri, bool requireSsl) { + CachedDirectWebResponse response; + try { + if (requireSsl && !string.Equals(uri.Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + Logger.Yadis.WarnFormat("Discovery on insecure identifier '{0}' aborted.", uri); + return null; + } + response = Request(requestHandler, uri, requireSsl, ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); + if (response.Status != System.Net.HttpStatusCode.OK) { + Logger.Yadis.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response.Status, response.Status, uri); + return null; + } + } catch (ArgumentException ex) { + // Unsafe URLs generate this + Logger.Yadis.WarnFormat("Unsafe OpenId URL detected ({0}). Request aborted. {1}", uri, ex); + return null; + } + CachedDirectWebResponse response2 = null; + if (IsXrdsDocument(response)) { + Logger.Yadis.Debug("An XRDS response was received from GET at user-supplied identifier."); + Reporting.RecordEventOccurrence("Yadis", "XRDS in initial response"); + response2 = response; + } else { + string uriString = response.Headers.Get(HeaderName); + Uri url = null; + if (uriString != null) { + if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) { + Logger.Yadis.DebugFormat("{0} found in HTTP header. Preparing to pull XRDS from {1}", HeaderName, url); + Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTTP header"); + } + } + if (url == null && response.ContentType != null && (response.ContentType.MediaType == ContentTypes.Html || response.ContentType.MediaType == ContentTypes.XHtml)) { + url = FindYadisDocumentLocationInHtmlMetaTags(response.GetResponseString()); + if (url != null) { + Logger.Yadis.DebugFormat("{0} found in HTML Http-Equiv tag. Preparing to pull XRDS from {1}", HeaderName, url); + Reporting.RecordEventOccurrence("Yadis", "XRDS referenced in HTML"); + } + } + if (url != null) { + if (!requireSsl || string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + response2 = Request(requestHandler, url, requireSsl, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); + if (response2.Status != HttpStatusCode.OK) { + Logger.Yadis.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response2.Status, response2.Status, uri); + } + } else { + Logger.Yadis.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url); + } + } + } + return new DiscoveryResult(uri, response, response2); + } + + /// <summary> + /// Searches an HTML document for a + /// <meta http-equiv="X-XRDS-Location" content="{YadisURL}"> + /// tag and returns the content of YadisURL. + /// </summary> + /// <param name="html">The HTML to search.</param> + /// <returns>The URI of the XRDS document if found; otherwise <c>null</c>.</returns> + public static Uri FindYadisDocumentLocationInHtmlMetaTags(string html) { + foreach (var metaTag in HtmlParser.HeadTags<HtmlMeta>(html)) { + if (HeaderName.Equals(metaTag.HttpEquiv, StringComparison.OrdinalIgnoreCase)) { + if (metaTag.Content != null) { + Uri uri; + if (Uri.TryCreate(metaTag.Content, UriKind.Absolute, out uri)) { + return uri; + } + } + } + } + return null; + } + + /// <summary> + /// Sends a YADIS HTTP request as part of identifier discovery. + /// </summary> + /// <param name="requestHandler">The request handler to use to actually submit the request.</param> + /// <param name="uri">The URI to GET.</param> + /// <param name="requireSsl">Whether only HTTPS URLs should ever be retrieved.</param> + /// <param name="acceptTypes">The value of the Accept HTTP header to include in the request.</param> + /// <returns>The HTTP response retrieved from the request.</returns> + internal static IncomingWebResponse Request(IDirectWebRequestHandler requestHandler, Uri uri, bool requireSsl, params string[] acceptTypes) { + Contract.Requires<ArgumentNullException>(requestHandler != null); + Contract.Requires<ArgumentNullException>(uri != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>() != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null); + + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); + request.CachePolicy = IdentifierDiscoveryCachePolicy; + if (acceptTypes != null) { + request.Accept = string.Join(",", acceptTypes); + } + + DirectWebRequestOptions options = DirectWebRequestOptions.None; + if (requireSsl) { + options |= DirectWebRequestOptions.RequireSsl; + } + + try { + return requestHandler.GetResponse(request, options); + } catch (ProtocolException ex) { + var webException = ex.InnerException as WebException; + if (webException != null) { + var response = webException.Response as HttpWebResponse; + if (response != null && response.IsFromCache) { + // We don't want to report error responses from the cache, since the server may have fixed + // whatever was causing the problem. So try again with cache disabled. + Logger.Messaging.Error("An HTTP error response was obtained from the cache. Retrying with cache disabled.", ex); + var nonCachingRequest = request.Clone(); + nonCachingRequest.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.Reload); + return requestHandler.GetResponse(nonCachingRequest, options); + } + } + + throw; + } + } + + /// <summary> + /// Determines whether a given HTTP response constitutes an XRDS document. + /// </summary> + /// <param name="response">The response to test.</param> + /// <returns> + /// <c>true</c> if the response constains an XRDS document; otherwise, <c>false</c>. + /// </returns> + private static bool IsXrdsDocument(CachedDirectWebResponse response) { + if (response.ContentType == null) { + return false; + } + + if (response.ContentType.MediaType == ContentTypes.Xrds) { + return true; + } + + if (response.ContentType.MediaType == ContentTypes.Xml) { + // This COULD be an XRDS document with an imprecise content-type. + response.ResponseStream.Seek(0, SeekOrigin.Begin); + XmlReader reader = XmlReader.Create(response.ResponseStream); + while (reader.Read() && reader.NodeType != XmlNodeType.Element) { + // intentionally blank + } + if (reader.NamespaceURI == XrdsNode.XrdsNamespace && reader.Name == "XRDS") { + return true; + } + } + + return false; + } + } +} |