diff options
Diffstat (limited to 'src/DotNetOpenAuth')
103 files changed, 4408 insertions, 1592 deletions
diff --git a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs index 37f9c78..980d90f 100644 --- a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs +++ b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs @@ -144,6 +144,7 @@ using System.Reflection; /// The conversion cannot be performed. /// </exception> public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { + Contract.Assume(destinationType != null, "Missing contract."); if (destinationType.IsInstanceOfType(value)) { return value; } diff --git a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs index 6ba9c4b..61c0fd8 100644 --- a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs +++ b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs @@ -45,7 +45,7 @@ namespace DotNetOpenAuth.ComponentModel { return null; } - MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public); + MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null); return CreateInstanceDescriptor(identifierParse, new object[] { value.ToString() }); } diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd index b639a29..3164ec5 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd @@ -319,6 +319,32 @@ </xs:documentation> </xs:annotation> </xs:attribute> + <xs:attribute name="allowDualPurposeIdentifiers" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether identifiers that are both OP Identifiers and Claimed Identifiers + should ever be recognized as claimed identifiers. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="allowApproximateIdentifierDiscovery" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls 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. + Only impacts hosts running under partial trust. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="protectDownlevelReplayAttacks" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether the relying party should take special care + to protect users against replay attacks when interoperating with OpenID 1.1 Providers. + </xs:documentation> + </xs:annotation> + </xs:attribute> </xs:complexType> </xs:element> <xs:element name="behaviors"> @@ -361,6 +387,27 @@ </xs:choice> </xs:complexType> </xs:element> + <xs:element name="discoveryServices"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="name" type="xs:string" use="required" /> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> <xs:element name="store"> <xs:annotation> <xs:documentation> diff --git a/src/DotNetOpenAuth/Configuration/MessagingElement.cs b/src/DotNetOpenAuth/Configuration/MessagingElement.cs index 9e957fe..f130dbc 100644 --- a/src/DotNetOpenAuth/Configuration/MessagingElement.cs +++ b/src/DotNetOpenAuth/Configuration/MessagingElement.cs @@ -32,6 +32,11 @@ namespace DotNetOpenAuth.Configuration { private const string MaximumClockSkewConfigName = "clockSkew"; /// <summary> + /// The name of the attribute that controls whether messaging rules are strictly followed. + /// </summary> + private const string StrictConfigName = "strict"; + + /// <summary> /// Gets the actual maximum message lifetime that a program should allow. /// </summary> /// <value>The sum of the <see cref="MaximumMessageLifetime"/> and @@ -83,6 +88,24 @@ namespace DotNetOpenAuth.Configuration { } /// <summary> + /// Gets or sets a value indicating whether messaging rules are strictly + /// adhered to. + /// </summary> + /// <value><c>true</c> by default.</value> + /// <remarks> + /// Strict will require that remote parties adhere strictly to the specifications, + /// even when a loose interpretation would not compromise security. + /// <c>true</c> is a good default because it shakes out interoperability bugs in remote services + /// so they can be identified and corrected. But some web sites want things to Just Work + /// more than they want to file bugs against others, so <c>false</c> is the setting for them. + /// </remarks> + [ConfigurationProperty(StrictConfigName, DefaultValue = true)] + internal bool Strict { + get { return (bool)this[StrictConfigName]; } + set { this[StrictConfigName] = value; } + } + + /// <summary> /// Gets or sets the configuration for the <see cref="UntrustedWebRequestHandler"/> class. /// </summary> /// <value>The untrusted web request.</value> diff --git a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartyElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartyElement.cs index cdf4fd3..2ee2e91 100644 --- a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartyElement.cs +++ b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartyElement.cs @@ -5,8 +5,10 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.Configuration { + using System; using System.Configuration; using System.Diagnostics.Contracts; + using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.RelyingParty; /// <summary> @@ -25,11 +27,21 @@ namespace DotNetOpenAuth.Configuration { private const string SecuritySettingsConfigName = "security"; /// <summary> - /// Gets the name of the <behaviors> sub-element. + /// 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() { @@ -62,5 +74,25 @@ namespace DotNetOpenAuth.Configuration { get { return (TypeConfigurationElement<IRelyingPartyApplicationStore>)this[StoreConfigName] ?? new TypeConfigurationElement<IRelyingPartyApplicationStore>(); } 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/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs index d10d9bd..1bf2ebc 100644 --- a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs +++ b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs @@ -66,6 +66,21 @@ namespace DotNetOpenAuth.Configuration { private const string PrivateSecretMaximumAgeConfigName = "privateSecretMaximumAge"; /// <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> /// Initializes a new instance of the <see cref="OpenIdRelyingPartySecuritySettingsElement"/> class. /// </summary> public OpenIdRelyingPartySecuritySettingsElement() { @@ -183,6 +198,43 @@ namespace DotNetOpenAuth.Configuration { } /// <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> /// 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> @@ -200,6 +252,9 @@ namespace DotNetOpenAuth.Configuration { settings.RejectUnsolicitedAssertions = this.RejectUnsolicitedAssertions; settings.RejectDelegatingIdentifiers = this.RejectDelegatingIdentifiers; settings.IgnoreUnsignedExtensions = this.IgnoreUnsignedExtensions; + settings.AllowDualPurposeIdentifiers = this.AllowDualPurposeIdentifiers; + settings.AllowApproximateIdentifierDiscovery = this.AllowApproximateIdentifierDiscovery; + settings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks; return settings; } diff --git a/src/DotNetOpenAuth/Configuration/ReportingElement.cs b/src/DotNetOpenAuth/Configuration/ReportingElement.cs index ab1437f..2374448 100644 --- a/src/DotNetOpenAuth/Configuration/ReportingElement.cs +++ b/src/DotNetOpenAuth/Configuration/ReportingElement.cs @@ -69,7 +69,7 @@ namespace DotNetOpenAuth.Configuration { /// Gets or sets a value indicating whether this reporting is enabled. /// </summary> /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> - [ConfigurationProperty(EnabledAttributeName, DefaultValue = false)] + [ConfigurationProperty(EnabledAttributeName, DefaultValue = true)] internal bool Enabled { get { return (bool)this[EnabledAttributeName]; } set { this[EnabledAttributeName] = value; } diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index f904c45..919d1f2 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -1,8 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> + <ProjectRoot Condition="'$(ProjectRoot)' == ''">$(MSBuildProjectDirectory)\..\..\</ProjectRoot> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <PropertyGroup> <ProductVersion>9.0.30729</ProductVersion> <SchemaVersion>2.0</SchemaVersion> <ProjectGuid>{3191B653-F76D-4C1A-9A5A-347BC3AAAAB7}</ProjectGuid> @@ -10,26 +14,46 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>DotNetOpenAuth</RootNamespace> <AssemblyName>DotNetOpenAuth</AssemblyName> - <TargetFrameworkVersion>v3.5</TargetFrameworkVersion> <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>DotNetOpenAuth.ico</ApplicationIcon> + <DocumentationFile>$(OutputPath)DotNetOpenAuth.xml</DocumentationFile> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> - <OutputPath>..\..\bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <AllowUnsafeBlocks>false</AllowUnsafeBlocks> - <DocumentationFile>..\..\bin\Debug\DotNetOpenAuth.xml</DocumentationFile> <RunCodeAnalysis>false</RunCodeAnalysis> - <CodeAnalysisRules>-Microsoft.Design#CA1054;-Microsoft.Design#CA1056;-Microsoft.Design#CA1055</CodeAnalysisRules> + <CodeAnalysisRules> + </CodeAnalysisRules> <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> <CodeContractsCustomRewriterAssembly> </CodeContractsCustomRewriterAssembly> @@ -58,18 +82,18 @@ http://opensource.org/licenses/ms-pl.html <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> - <OutputPath>..\..\bin\Release\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> <AllowUnsafeBlocks>false</AllowUnsafeBlocks> - <DocumentationFile>..\..\bin\Release\DotNetOpenAuth.xml</DocumentationFile> <RunCodeAnalysis>true</RunCodeAnalysis> - <CodeAnalysisRules>-Microsoft.Design#CA1054;-Microsoft.Design#CA1056;-Microsoft.Design#CA1055</CodeAnalysisRules> + <CodeAnalysisRules> + </CodeAnalysisRules> <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> <CodeContractsCustomRewriterAssembly> </CodeContractsCustomRewriterAssembly> @@ -98,18 +122,15 @@ http://opensource.org/licenses/ms-pl.html <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> - </PropertyGroup> - <PropertyGroup> - <SignAssembly>true</SignAssembly> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeAnalysis|AnyCPU' "> <DebugSymbols>true</DebugSymbols> - <OutputPath>..\..\bin\CodeAnalysis\</OutputPath> <DefineConstants>$(DefineConstants);CONTRACTS_FULL;DEBUG;TRACE</DefineConstants> - <DocumentationFile>..\..\bin\CodeAnalysis\DotNetOpenAuth.xml</DocumentationFile> <DebugType>full</DebugType> <PlatformTarget>AnyCPU</PlatformTarget> - <CodeAnalysisRules>-Microsoft.Design#CA1054;-Microsoft.Design#CA1056;-Microsoft.Design#CA1055</CodeAnalysisRules> + <CodeAnalysisRules> + </CodeAnalysisRules> <CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression> <CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile> <ErrorReport>prompt</ErrorReport> @@ -142,15 +163,11 @@ http://opensource.org/licenses/ms-pl.html <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> <Reference Include="log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> - <HintPath>..\..\lib\log4net.dll</HintPath> - </Reference> - <Reference Include="Microsoft.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=736440c9b414ea16, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\..\lib\Microsoft.Contracts.dll</HintPath> </Reference> <Reference Include="PresentationFramework"> <RequiredTargetFramework>3.0</RequiredTargetFramework> @@ -178,9 +195,7 @@ http://opensource.org/licenses/ms-pl.html <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> <Reference Include="System.Web" /> - <Reference Include="System.Web.Abstractions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\..\lib\System.Web.Abstractions.dll</HintPath> + <Reference Include="System.Web.Abstractions"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> <Reference Include="System.Web.Extensions"> @@ -189,16 +204,11 @@ http://opensource.org/licenses/ms-pl.html <Reference Include="System.Web.Extensions.Design"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="System.Web.Mobile" /> - <Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\..\lib\System.Web.Mvc.dll</HintPath> - </Reference> - <Reference Include="System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\..\lib\System.Web.Routing.dll</HintPath> + <Reference Include="System.Web.Mobile" Condition=" '$(ClrVersion)' != '4' " /> + <Reference Include="System.Web.Routing"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> + <Reference Include="System.Xaml" Condition=" '$(ClrVersion)' == '4' " /> <Reference Include="System.XML" /> <Reference Include="System.Xml.Linq"> <RequiredTargetFramework>3.5</RequiredTargetFramework> @@ -206,6 +216,18 @@ http://opensource.org/licenses/ms-pl.html <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\ClaimTypeSuggestions.cs" /> @@ -276,7 +298,10 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="Messaging\OutgoingWebResponseActionResult.cs" /> <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> <Compile Include="Messaging\Reflection\IMessagePartNullEncoder.cs" /> + <Compile Include="Messaging\Reflection\IMessagePartOriginalEncoder.cs" /> <Compile Include="Messaging\Reflection\MessageDescriptionCollection.cs" /> + <Compile Include="Mvc\OpenIdHelper.cs" /> + <Compile Include="Mvc\OpenIdAjaxOptions.cs" /> <Compile Include="OAuth\ChannelElements\ICombinedOpenIdProviderTokenManager.cs" /> <Compile Include="OAuth\ChannelElements\IConsumerDescription.cs" /> <Compile Include="OAuth\ChannelElements\IConsumerTokenManager.cs" /> @@ -302,6 +327,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OAuth\ConsumerSecuritySettings.cs" /> <Compile Include="OAuth\DesktopConsumer.cs" /> <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="Messaging\IMessageWithBinaryData.cs" /> <Compile Include="OAuth\Messages\ITokenSecretContainingMessage.cs" /> <Compile Include="Messaging\ChannelEventArgs.cs" /> <Compile Include="Messaging\ITamperProtectionChannelBindingElement.cs" /> @@ -429,12 +455,14 @@ http://opensource.org/licenses/ms-pl.html <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" /> @@ -501,6 +529,9 @@ http://opensource.org/licenses/ms-pl.html <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\OpenIdAjaxRelyingParty.cs" /> <Compile Include="OpenId\RelyingParty\SelectorButtonContract.cs" /> <Compile Include="OpenId\RelyingParty\SelectorProviderButton.cs" /> <Compile Include="OpenId\RelyingParty\SelectorOpenIdButton.cs" /> @@ -508,14 +539,13 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\RelyingParty\IRelyingPartyBehavior.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdSelector.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdRelyingPartyAjaxControlBase.cs" /> - <Compile Include="OpenId\RelyingParty\IXrdsProviderEndpointContract.cs" /> <Compile Include="OpenId\RelyingParty\IAuthenticationRequestContract.cs" /> <Compile Include="OpenId\RelyingParty\NegativeAuthenticationResponse.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdAjaxTextBox.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdButton.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdEventArgs.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdLogin.cs" /> - <Compile Include="OpenId\RelyingParty\OpenIdMobileTextBox.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdMobileTextBox.cs" Condition=" '$(ClrVersion)' != '4' " /> <Compile Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdTextBox.cs" /> <Compile Include="OpenId\RelyingParty\PopupBehavior.cs" /> @@ -525,9 +555,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\RelyingParty\FailedAuthenticationResponse.cs" /> <Compile Include="OpenId\RelyingParty\IAuthenticationRequest.cs" /> <Compile Include="OpenId\RelyingParty\IAuthenticationResponse.cs" /> - <Compile Include="OpenId\RelyingParty\IProviderEndpoint.cs" /> <Compile Include="OpenId\RelyingParty\ISetupRequiredAuthenticationResponse.cs" /> - <Compile Include="OpenId\RelyingParty\IXrdsProviderEndpoint.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdRelyingParty.cs" /> <Compile Include="OpenId\OpenIdStrings.Designer.cs"> <DependentUpon>OpenIdStrings.resx</DependentUpon> @@ -542,7 +570,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\RelyingParty\PrivateSecretManager.cs" /> <Compile Include="OpenId\RelyingParty\RelyingPartySecuritySettings.cs" /> <Compile Include="OpenId\RelyingParty\SelectorButton.cs" /> - <Compile Include="OpenId\RelyingParty\ServiceEndpoint.cs" /> + <Compile Include="OpenId\IdentifierDiscoveryResult.cs" /> <Compile Include="OpenId\OpenIdXrdsHelper.cs" /> <Compile Include="OpenId\RelyingParty\SimpleXrdsProviderEndpoint.cs" /> <Compile Include="OpenId\RelyingParty\StandardRelyingPartyApplicationStore.cs" /> @@ -550,7 +578,9 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\RelyingParty\WellKnownProviders.cs" /> <Compile Include="OpenId\SecuritySettings.cs" /> <Compile Include="Messaging\UntrustedWebRequestHandler.cs" /> + <Compile Include="OpenId\UriDiscoveryService.cs" /> <Compile Include="OpenId\UriIdentifier.cs" /> + <Compile Include="OpenId\XriDiscoveryProxyService.cs" /> <Compile Include="OpenId\XriIdentifier.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="OAuth\Messages\UnauthorizedTokenRequest.cs" /> @@ -586,6 +616,7 @@ http://opensource.org/licenses/ms-pl.html </ItemGroup> <ItemGroup> <None Include="Configuration\DotNetOpenAuth.xsd" /> + <None Include="Migrated rules for DotNetOpenAuth.ruleset" /> <None Include="OAuth\ClassDiagram.cd" /> <None Include="OAuth\Messages\OAuth Messages.cd" /> <None Include="Messaging\Bindings\Bindings.cd" /> @@ -618,7 +649,7 @@ http://opensource.org/licenses/ms-pl.html </EmbeddedResource> </ItemGroup> <ItemGroup> - <EmbeddedResource Include="OpenId\RelyingParty\openid_login.gif" /> + <EmbeddedResource Include="OpenId\RelyingParty\openid_login.png" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="OpenId\RelyingParty\login_failure.png" /> @@ -681,14 +712,49 @@ http://opensource.org/licenses/ms-pl.html <ItemGroup> <EmbeddedResource Include="OpenId\RelyingParty\OpenIdSelector.css" /> </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" /> - <Import Project="..\..\tools\JavascriptPacker.targets" /> - <PropertyGroup> - <CompileDependsOn>$(CompileDependsOn);CheckForCodeContracts</CompileDependsOn> - </PropertyGroup> - <Target Name="CheckForCodeContracts"> - <Error Condition=" '$(CodeContractsImported)' != 'true' " - Text="This project requires Code Contracts. Please install from: http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx"/> + <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> + <Content Include="DotNetOpenAuth.ico" /> + </ItemGroup> + + <ItemGroup> + <SignDependsOn Include="BuildUnifiedProduct" /> + <DelaySignedAssemblies Include="$(ILMergeOutputAssembly); + $(OutputPath)CodeContracts\$(ProductName).Contracts.dll; + " /> + </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)"> + <MakeDir Directories="$(ILMergeOutputAssemblyDirectory)" /> + <ILMerge ExcludeFile="$(ProjectRoot)ILMergeInternalizeExceptions.txt" + InputAssemblies="@(ILMergeInputAssemblies)" + OutputFile="$(ILMergeOutputAssembly)" + KeyFile="$(PublicKeyFile)" + DelaySign="true" + /> </Target> + + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> </Project> diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.ico b/src/DotNetOpenAuth/DotNetOpenAuth.ico Binary files differnew file mode 100644 index 0000000..e227dbe --- /dev/null +++ b/src/DotNetOpenAuth/DotNetOpenAuth.ico diff --git a/src/DotNetOpenAuth/GlobalSuppressions.cs b/src/DotNetOpenAuth/GlobalSuppressions.cs index e436846..9b1bcfa 100644 --- a/src/DotNetOpenAuth/GlobalSuppressions.cs +++ b/src/DotNetOpenAuth/GlobalSuppressions.cs @@ -57,3 +57,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnIncomingRequest(DotNetOpenAuth.OpenId.Provider.IRequest)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.ApplySecuritySettings(DotNetOpenAuth.OpenId.Provider.ProviderSecuritySettings)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Mvc", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Portability", "CA1903:UseOnlyApiFromTargetedFramework", MessageId = "System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")] diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs index 86c1118..ae45229 100644 --- a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs +++ b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs @@ -268,6 +268,7 @@ namespace DotNetOpenAuth.InfoCard { [Category(InfoCardCategory), DefaultValue(PrivacyUrlDefault)] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "We construct a Uri to validate the format of the string.")] [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "That overload is NOT the same.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This can take ~/ paths.")] public string PrivacyUrl { get { return (string)this.ViewState[PrivacyUrlViewStateKey] ?? PrivacyUrlDefault; @@ -570,24 +571,28 @@ namespace DotNetOpenAuth.InfoCard { Panel supportedPanel = new Panel(); - if (!this.DesignMode) { - // At the user agent, assume InfoCard is not supported until - // the JavaScript discovers otherwise and reveals this panel. - supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; - } + try { + if (!this.DesignMode) { + // At the user agent, assume InfoCard is not supported until + // the JavaScript discovers otherwise and reveals this panel. + supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; + } - supportedPanel.Controls.Add(this.CreateInfoCardImage()); + supportedPanel.Controls.Add(this.CreateInfoCardImage()); - // trigger the selector at page load? - if (this.AutoPopup && !this.Page.IsPostBack) { - this.Page.ClientScript.RegisterStartupScript( - typeof(InfoCardSelector), - "selector_load_trigger", - this.GetInfoCardSelectorActivationScript(true), - true); + // trigger the selector at page load? + if (this.AutoPopup && !this.Page.IsPostBack) { + this.Page.ClientScript.RegisterStartupScript( + typeof(InfoCardSelector), + "selector_load_trigger", + this.GetInfoCardSelectorActivationScript(true), + true); + } + return supportedPanel; + } catch { + supportedPanel.Dispose(); + throw; } - - return supportedPanel; } /// <summary> @@ -624,10 +629,15 @@ namespace DotNetOpenAuth.InfoCard { Contract.Ensures(Contract.Result<Panel>() != null); Panel unsupportedPanel = new Panel(); - if (this.UnsupportedTemplate != null) { - this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); + try { + if (this.UnsupportedTemplate != null) { + this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); + } + return unsupportedPanel; + } catch { + unsupportedPanel.Dispose(); + throw; } - return unsupportedPanel; } /// <summary> @@ -692,13 +702,18 @@ namespace DotNetOpenAuth.InfoCard { private Image CreateInfoCardImage() { // add clickable image Image image = new Image(); - image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); - image.AlternateText = InfoCardStrings.SelectorClickPrompt; - image.ToolTip = this.ToolTip; - image.Style[HtmlTextWriterStyle.Cursor] = "hand"; - - image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); - return image; + try { + image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); + image.AlternateText = InfoCardStrings.SelectorClickPrompt; + image.ToolTip = this.ToolTip; + image.Style[HtmlTextWriterStyle.Cursor] = "hand"; + + image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); + return image; + } catch { + image.Dispose(); + throw; + } } /// <summary> diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardStrings.Designer.cs b/src/DotNetOpenAuth/InfoCard/InfoCardStrings.Designer.cs index 00eb1af..a6d3dcf 100644 --- a/src/DotNetOpenAuth/InfoCard/InfoCardStrings.Designer.cs +++ b/src/DotNetOpenAuth/InfoCard/InfoCardStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.4918 +// Runtime Version:4.0.30104.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth.InfoCard { // 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", "2.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class InfoCardStrings { diff --git a/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs b/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs index 124f9f8..2ac2b7e 100644 --- a/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs +++ b/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs @@ -74,7 +74,13 @@ namespace DotNetOpenAuth.InfoCard { public void AddDecryptingToken(X509Certificate2 certificate) { Contract.Requires<ArgumentNullException>(certificate != null); Contract.Requires<ArgumentException>(certificate.HasPrivateKey); - this.AddDecryptingToken(new X509SecurityToken(certificate)); + var cert = new X509SecurityToken(certificate); + try { + this.AddDecryptingToken(cert); + } catch { + cert.Dispose(); + throw; + } } #if CONTRACTS_FULL diff --git a/src/DotNetOpenAuth/InfoCard/Token/Token.cs b/src/DotNetOpenAuth/InfoCard/Token/Token.cs index 7fa9a95..89fa3a3 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/Token.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/Token.cs @@ -49,16 +49,18 @@ namespace DotNetOpenAuth.InfoCard { byte[] decryptedBytes; string decryptedString; - using (XmlReader tokenReader = XmlReader.Create(new StringReader(tokenXml))) { - Contract.Assume(tokenReader != null); // BCL contract should say XmlReader.Create result != null - if (IsEncrypted(tokenReader)) { - Logger.InfoCard.DebugFormat("Incoming SAML token, before decryption: {0}", tokenXml); - decryptedBytes = decryptor.DecryptToken(tokenReader); - decryptedString = Encoding.UTF8.GetString(decryptedBytes); - Contract.Assume(decryptedString != null); // BCL contracts should be enhanced here - } else { - decryptedBytes = Encoding.UTF8.GetBytes(tokenXml); - decryptedString = tokenXml; + using (StringReader xmlReader = new StringReader(tokenXml)) { + using (XmlReader tokenReader = XmlReader.Create(xmlReader)) { + Contract.Assume(tokenReader != null); // BCL contract should say XmlReader.Create result != null + if (IsEncrypted(tokenReader)) { + Logger.InfoCard.DebugFormat("Incoming SAML token, before decryption: {0}", tokenXml); + decryptedBytes = decryptor.DecryptToken(tokenReader); + decryptedString = Encoding.UTF8.GetString(decryptedBytes); + Contract.Assume(decryptedString != null); // BCL contracts should be enhanced here + } else { + decryptedBytes = Encoding.UTF8.GetBytes(tokenXml); + decryptedString = tokenXml; + } } } diff --git a/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs b/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs index 48b7794..4ac871a 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs @@ -226,7 +226,9 @@ namespace DotNetOpenAuth.InfoCard { int charMapLength = charMap.Length; byte[] raw = Convert.FromBase64String(ppid); - raw = SHA1.Create().ComputeHash(raw); + using (HashAlgorithm hasher = SHA1.Create()) { + raw = hasher.ComputeHash(raw); + } StringBuilder callSign = new StringBuilder(); diff --git a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs index dd34d90..c9bc1d3 100644 --- a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs @@ -90,11 +90,16 @@ namespace DotNetOpenAuth.Messaging { public override StreamReader GetResponseReader() { this.ResponseStream.Seek(0, SeekOrigin.Begin); string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - if (string.IsNullOrEmpty(contentEncoding)) { - return new StreamReader(this.ResponseStream); - } else { - return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); + Encoding encoding = null; + if (!string.IsNullOrEmpty(contentEncoding)) { + try { + encoding = Encoding.GetEncoding(contentEncoding); + } catch (ArgumentException ex) { + Logger.Messaging.ErrorFormat("Encoding.GetEncoding(\"{0}\") threw ArgumentException: {1}", contentEncoding, ex); + } } + + return encoding != null ? new StreamReader(this.ResponseStream, encoding) : new StreamReader(this.ResponseStream); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs index 831283a..cc411c8 100644 --- a/src/DotNetOpenAuth/Messaging/Channel.cs +++ b/src/DotNetOpenAuth/Messaging/Channel.cs @@ -695,27 +695,28 @@ namespace DotNetOpenAuth.Messaging { WebHeaderCollection headers = new WebHeaderCollection(); headers.Add(HttpResponseHeader.ContentType, "text/html"); - StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture); - StringBuilder hiddenFields = new StringBuilder(); - foreach (var field in fields) { - hiddenFields.AppendFormat( - "\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n", - HttpUtility.HtmlEncode(field.Key), - HttpUtility.HtmlEncode(field.Value)); + using (StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture)) { + StringBuilder hiddenFields = new StringBuilder(); + foreach (var field in fields) { + hiddenFields.AppendFormat( + "\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n", + HttpUtility.HtmlEncode(field.Key), + HttpUtility.HtmlEncode(field.Value)); + } + bodyWriter.WriteLine( + IndirectMessageFormPostFormat, + HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), + hiddenFields); + bodyWriter.Flush(); + OutgoingWebResponse response = new OutgoingWebResponse { + Status = HttpStatusCode.OK, + Headers = headers, + Body = bodyWriter.ToString(), + OriginalMessage = message + }; + + return response; } - bodyWriter.WriteLine( - IndirectMessageFormPostFormat, - HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), - hiddenFields); - bodyWriter.Flush(); - OutgoingWebResponse response = new OutgoingWebResponse { - Status = HttpStatusCode.OK, - Headers = headers, - Body = bodyWriter.ToString(), - OriginalMessage = message - }; - - return response; } /// <summary> @@ -872,7 +873,18 @@ namespace DotNetOpenAuth.Messaging { HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); httpRequest.CachePolicy = this.CachePolicy; httpRequest.Method = "POST"; - this.SendParametersInEntity(httpRequest, fields); + + var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData; + if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) { + var multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData); + + // When sending multi-part, all data gets send as multi-part -- even the non-binary data. + multiPartFields.AddRange(fields.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value))); + this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields); + } else { + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); + this.SendParametersInEntity(httpRequest, fields); + } return httpRequest; } @@ -942,6 +954,19 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Sends the given parameters in the entity stream of an HTTP request in multi-part format. + /// </summary> + /// <param name="httpRequest">The HTTP request.</param> + /// <param name="fields">The parameters to send.</param> + /// <remarks> + /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes + /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>. + /// </remarks> + protected void SendParametersInEntityAsMultipart(HttpWebRequest httpRequest, IEnumerable<MultipartPostPart> fields) { + httpRequest.PostMultipartNoGetResponse(this.WebRequestHandler, fields); + } + + /// <summary> /// Verifies the integrity and applicability of an incoming message. /// </summary> /// <param name="message">The message just received.</param> @@ -953,7 +978,7 @@ namespace DotNetOpenAuth.Messaging { Contract.Requires<ArgumentNullException>(message != null); if (Logger.Channel.IsInfoEnabled) { - var messageAccessor = this.MessageDescriptions.GetAccessor(message); + var messageAccessor = this.MessageDescriptions.GetAccessor(message, true); Logger.Channel.InfoFormat( "Processing incoming {0} ({1}) message:{2}{3}", message.GetType().Name, diff --git a/src/DotNetOpenAuth/Messaging/IMessageWithBinaryData.cs b/src/DotNetOpenAuth/Messaging/IMessageWithBinaryData.cs new file mode 100644 index 0000000..f411cf5 --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/IMessageWithBinaryData.cs @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------- +// <copyright file="IMessageWithBinaryData.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// The interface that classes must implement to be serialized/deserialized + /// as protocol or extension messages that uses POST multi-part data for binary content. + /// </summary> + [ContractClass(typeof(IMessageWithBinaryDataContract))] + public interface IMessageWithBinaryData : IDirectedProtocolMessage { + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + IList<MultipartPostPart> BinaryData { get; } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + bool SendAsMultipart { get; } + } + + /// <summary> + /// The contract class for the <see cref="IMessageWithBinaryData"/> interface. + /// </summary> + [ContractClassFor(typeof(IMessageWithBinaryData))] + internal sealed class IMessageWithBinaryDataContract : IMessageWithBinaryData { + #region IMessageWithBinaryData Members + + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + IList<MultipartPostPart> IMessageWithBinaryData.BinaryData { + get { + Contract.Ensures(Contract.Result<IList<MultipartPostPart>>() != null); + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + bool IMessageWithBinaryData.SendAsMultipart { + get { throw new NotImplementedException(); } + } + + #endregion + + #region IMessage Properties + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + /// <value></value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + Version IMessage.Version { + get { + Contract.Ensures(Contract.Result<Version>() != null); + return default(Version); // dummy return + } + } + + /// <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> + IDictionary<string, string> IMessage.ExtraData { + get { + Contract.Ensures(Contract.Result<IDictionary<string, string>>() != null); + return default(IDictionary<string, string>); + } + } + + #endregion + + #region IDirectedProtocolMessage Members + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + /// <remarks> + /// For indirect messages this will likely be GET+POST, which both can be simulated in the user agent: + /// the GET with a simple 301 Redirect, and the POST with an HTML form in the response with javascript + /// to automate submission. + /// </remarks> + HttpDeliveryMethods IDirectedProtocolMessage.HttpMethods { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the URL of the intended receiver of this message. + /// </summary> + Uri IDirectedProtocolMessage.Recipient { + get { throw new NotImplementedException(); } + } + + #endregion + + #region IProtocolMessage Members + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + MessageProtections IProtocolMessage.RequiredProtection { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + MessageTransport IProtocolMessage.Transport { + get { throw new NotImplementedException(); } + } + + #endregion + + #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() { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs index f2c9add..6f8c4f9 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.4918 +// Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth.Messaging { // 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", "2.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class MessagingStrings { @@ -70,6 +70,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.. + /// </summary> + internal static string BinaryDataRequiresMultipart { + get { + return ResourceManager.GetString("BinaryDataRequiresMultipart", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to HttpContext.Current is null. There must be an ASP.NET request in process for this operation to succeed.. /// </summary> internal static string CurrentHttpContextRequired { @@ -385,6 +394,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to An HttpContext.Current.Session object is required.. + /// </summary> + internal static string SessionRequired { + get { + return ResourceManager.GetString("SessionRequired", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Message signature was incorrect.. /// </summary> internal static string SignatureInvalid { diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx index 3d4e317..bdf4f68 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx @@ -297,4 +297,10 @@ <data name="StreamMustHaveKnownLength" xml:space="preserve"> <value>The stream must have a known length.</value> </data> + <data name="BinaryDataRequiresMultipart" xml:space="preserve"> + <value>Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.</value> + </data> + <data name="SessionRequired" xml:space="preserve"> + <value>An HttpContext.Current.Session object is required.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 04d91de..6a55bc9 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -89,7 +89,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult. /// </summary> - /// <param name="response">The response to send to the uesr agent.</param> + /// <param name="response">The response to send to the user agent.</param> /// <returns>The <see cref="ActionResult"/> instance to be returned by the Controller's action method.</returns> public static ActionResult AsActionResult(this OutgoingWebResponse response) { Contract.Requires<ArgumentNullException>(response != null); @@ -107,14 +107,7 @@ namespace DotNetOpenAuth.Messaging { Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); HttpContext context = HttpContext.Current; - // We use Request.Url for the full path to the server, and modify it - // with Request.RawUrl to capture both the cookieless session "directory" if it exists - // and the original path in case URL rewriting is going on. We don't want to be - // fooled by URL rewriting because we're comparing the actual URL with what's in - // the return_to parameter in some cases. - // Response.ApplyAppPathModifier(builder.Path) would have worked for the cookieless - // session, but not the URL rewriting problem. - return new Uri(context.Request.Url, context.Request.RawUrl); + return HttpRequestInfo.GetPublicFacingUrl(context.Request, context.Request.ServerVariables); } /// <summary> @@ -153,6 +146,63 @@ namespace DotNetOpenAuth.Messaging { Contract.Requires<ArgumentNullException>(requestHandler != null); Contract.Requires<ArgumentNullException>(parts != null); + PostMultipartNoGetResponse(request, requestHandler, parts); + return requestHandler.GetResponse(request); + } + + /// <summary> + /// Assembles a message comprised of the message on a given exception and all inner exceptions. + /// </summary> + /// <param name="exception">The exception.</param> + /// <returns>The assembled message.</returns> + public static string ToStringDescriptive(this Exception exception) { + // The input being null is probably bad, but since this method is called + // from a catch block, we don't really want to throw a new exception and + // hide the details of this one. + if (exception == null) { + Logger.Messaging.Error("MessagingUtilities.GetAllMessages called with null input."); + } + + StringBuilder message = new StringBuilder(); + while (exception != null) { + message.Append(exception.Message); + exception = exception.InnerException; + if (exception != null) { + message.Append(" "); + } + } + + return message.ToString(); + } + + /// <summary> + /// Flattens the specified sequence of sequences. + /// </summary> + /// <typeparam name="T">The type of element contained in the sequence.</typeparam> + /// <param name="sequence">The sequence of sequences to flatten.</param> + /// <returns>A sequence of the contained items.</returns> + [Obsolete("Use Enumerable.SelectMany instead.")] + public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> sequence) { + ErrorUtilities.VerifyArgumentNotNull(sequence, "sequence"); + + foreach (IEnumerable<T> subsequence in sequence) { + foreach (T item in subsequence) { + yield return item; + } + } + } + + /// <summary> + /// Sends a multipart HTTP POST request (useful for posting files) but doesn't call GetResponse on it. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <param name="requestHandler">The request handler.</param> + /// <param name="parts">The parts to include in the POST entity.</param> + internal static void PostMultipartNoGetResponse(this HttpWebRequest request, IDirectWebRequestHandler requestHandler, IEnumerable<MultipartPostPart> parts) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + Contract.Requires<ArgumentNullException>(parts != null); + Reporting.RecordFeatureUse("MessagingUtilities.PostMultipart"); parts = parts.CacheGeneratedResults(); string boundary = Guid.NewGuid().ToString(); @@ -193,33 +243,6 @@ namespace DotNetOpenAuth.Messaging { requestStream.Dispose(); } } - - return requestHandler.GetResponse(request); - } - - /// <summary> - /// Assembles a message comprised of the message on a given exception and all inner exceptions. - /// </summary> - /// <param name="exception">The exception.</param> - /// <returns>The assembled message.</returns> - public static string ToStringDescriptive(this Exception exception) { - // The input being null is probably bad, but since this method is called - // from a catch block, we don't really want to throw a new exception and - // hide the details of this one. - if (exception == null) { - Logger.Messaging.Error("MessagingUtilities.GetAllMessages called with null input."); - } - - StringBuilder message = new StringBuilder(); - while (exception != null) { - message.Append(exception.Message); - exception = exception.InnerException; - if (exception != null) { - message.Append(" "); - } - } - - return message.ToString(); } /// <summary> @@ -324,6 +347,7 @@ namespace DotNetOpenAuth.Messaging { } } +#if !CLR4 /// <summary> /// Copies the contents of one stream to another. /// </summary> @@ -339,8 +363,9 @@ namespace DotNetOpenAuth.Messaging { Contract.Requires<ArgumentNullException>(copyTo != null); Contract.Requires<ArgumentException>(copyFrom.CanRead, MessagingStrings.StreamUnreadable); Contract.Requires<ArgumentException>(copyTo.CanWrite, MessagingStrings.StreamUnwritable); - return CopyTo(copyFrom, copyTo, int.MaxValue); + return CopyUpTo(copyFrom, copyTo, int.MaxValue); } +#endif /// <summary> /// Copies the contents of one stream to another. @@ -353,7 +378,7 @@ namespace DotNetOpenAuth.Messaging { /// Copying begins at the streams' current positions. /// The positions are NOT reset after copying is complete. /// </remarks> - internal static int CopyTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy) { + internal static int CopyUpTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy) { Contract.Requires<ArgumentNullException>(copyFrom != null); Contract.Requires<ArgumentNullException>(copyTo != null); Contract.Requires<ArgumentException>(copyFrom.CanRead, MessagingStrings.StreamUnreadable); @@ -755,7 +780,10 @@ namespace DotNetOpenAuth.Messaging { if (throwOnNullKey) { throw new ArgumentException(MessagingStrings.UnexpectedNullKey); } else { - Logger.OpenId.WarnFormat("Null key with value {0} encountered while translating NameValueCollection to Dictionary.", nvc[key]); + // Only emit a warning if there was a non-empty value. + if (!string.IsNullOrEmpty(nvc[key])) { + Logger.OpenId.WarnFormat("Null key with value {0} encountered while translating NameValueCollection to Dictionary.", nvc[key]); + } } } else { dictionary.Add(key, nvc[key]); @@ -847,7 +875,7 @@ namespace DotNetOpenAuth.Messaging { /// by using appropriate character escaping. /// </summary> /// <param name="value">The untrusted string value to be escaped to protected against XSS attacks. May be null.</param> - /// <returns>The escaped string.</returns> + /// <returns>The escaped string, surrounded by single-quotes.</returns> internal static string GetSafeJavascriptValue(string value) { if (value == null) { return "null"; diff --git a/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs index 06cb5fc..cc655cf 100644 --- a/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs @@ -57,7 +57,7 @@ namespace DotNetOpenAuth.Messaging { this.ResponseStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : (int)response.ContentLength); using (Stream responseStream = response.GetResponseStream()) { // BUGBUG: strictly speaking, is the response were exactly the limit, we'd report it as truncated here. - this.IsResponseTruncated = responseStream.CopyTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead; + this.IsResponseTruncated = responseStream.CopyUpTo(this.ResponseStream, maximumBytesToRead) == maximumBytesToRead; this.ResponseStream.Seek(0, SeekOrigin.Begin); } } @@ -183,7 +183,10 @@ namespace DotNetOpenAuth.Messaging { /// would transmit the message that normally would be transmitted via a user agent redirect. /// </summary> /// <param name="channel">The channel to use for encoding.</param> - /// <returns>The URL that would transmit the original message.</returns> + /// <returns> + /// The URL that would transmit the original message. This URL may exceed the normal 2K limit, + /// and should therefore be broken up manually and POSTed as form fields when it exceeds this length. + /// </returns> /// <remarks> /// This is useful for desktop applications that will spawn a user agent to transmit the message /// rather than cause a redirect. diff --git a/src/DotNetOpenAuth/Messaging/ProtocolException.cs b/src/DotNetOpenAuth/Messaging/ProtocolException.cs index 25f8eee..fb9ec6d 100644 --- a/src/DotNetOpenAuth/Messaging/ProtocolException.cs +++ b/src/DotNetOpenAuth/Messaging/ProtocolException.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.Messaging { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; + using System.Security; using System.Security.Permissions; /// <summary> @@ -79,7 +80,11 @@ namespace DotNetOpenAuth.Messaging { /// <IPermission class="System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Read="*AllFiles*" PathDiscovery="*AllFiles*"/> /// <IPermission class="System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" version="1" Flags="SerializationFormatter"/> /// </PermissionSet> +#if CLR4 + [SecurityCritical] +#else [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] +#endif public override void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { base.GetObjectData(info, context); throw new NotImplementedException(); diff --git a/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartOriginalEncoder.cs b/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartOriginalEncoder.cs new file mode 100644 index 0000000..9ad55c9 --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartOriginalEncoder.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// <copyright file="IMessagePartOriginalEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Reflection { + /// <summary> + /// An interface describing how various objects can be serialized and deserialized between their object and string forms. + /// </summary> + /// <remarks> + /// Implementations of this interface must include a default constructor and must be thread-safe. + /// </remarks> + public interface IMessagePartOriginalEncoder : IMessagePartEncoder { + /// <summary> + /// Encodes the specified value as the original value that was formerly decoded. + /// </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> + string EncodeAsOriginalString(object value); + } +} diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs index 5493ba6..3b41b35 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescription.cs @@ -66,7 +66,20 @@ namespace DotNetOpenAuth.Messaging.Reflection { internal MessageDictionary GetDictionary(IMessage message) { Contract.Requires<ArgumentNullException>(message != null); Contract.Ensures(Contract.Result<MessageDictionary>() != null); - return new MessageDictionary(message, this); + return this.GetDictionary(message, false); + } + + /// <summary> + /// Gets a dictionary that provides read/write access to a message. + /// </summary> + /// <param name="message">The message the dictionary should provide access to.</param> + /// <param name="getOriginalValues">A value indicating whether this message dictionary will retrieve original values instead of normalized ones.</param> + /// <returns>The dictionary accessor to the message</returns> + [Pure] + internal MessageDictionary GetDictionary(IMessage message, bool getOriginalValues) { + Contract.Requires<ArgumentNullException>(message != null); + Contract.Ensures(Contract.Result<MessageDictionary>() != null); + return new MessageDictionary(message, this, getOriginalValues); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs index ff8b74b..125742c 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDescriptionCollection.cs @@ -78,7 +78,19 @@ namespace DotNetOpenAuth.Messaging.Reflection { [Pure] internal MessageDictionary GetAccessor(IMessage message) { Contract.Requires<ArgumentNullException>(message != null); - return this.Get(message).GetDictionary(message); + return this.GetAccessor(message, false); + } + + /// <summary> + /// Gets the dictionary that provides read/write access to a message. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="getOriginalValues">A value indicating whether this message dictionary will retrieve original values instead of normalized ones.</param> + /// <returns>The dictionary.</returns> + [Pure] + internal MessageDictionary GetAccessor(IMessage message, bool getOriginalValues) { + Contract.Requires<ArgumentNullException>(message != null); + return this.Get(message).GetDictionary(message, getOriginalValues); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs index f6eed24..2b60a9c 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs @@ -30,17 +30,24 @@ namespace DotNetOpenAuth.Messaging.Reflection { private readonly MessageDescription description; /// <summary> + /// Whether original string values should be retrieved instead of normalized ones. + /// </summary> + private readonly bool getOriginalValues; + + /// <summary> /// Initializes a new instance of the <see cref="MessageDictionary"/> class. /// </summary> /// <param name="message">The message instance whose values will be manipulated by this dictionary.</param> /// <param name="description">The message description.</param> + /// <param name="getOriginalValues">A value indicating whether this message dictionary will retrieve original values instead of normalized ones.</param> [Pure] - internal MessageDictionary(IMessage message, MessageDescription description) { + internal MessageDictionary(IMessage message, MessageDescription description, bool getOriginalValues) { Contract.Requires<ArgumentNullException>(message != null); Contract.Requires<ArgumentNullException>(description != null); this.message = message; this.description = description; + this.getOriginalValues = getOriginalValues; } /// <summary> @@ -103,7 +110,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { List<string> keys = new List<string>(this.description.Mapping.Count); foreach (var pair in this.description.Mapping) { // Don't include keys with null values, but default values for structs is ok - if (pair.Value.GetValue(this.message) != null) { + if (pair.Value.GetValue(this.message, this.getOriginalValues) != null) { keys.Add(pair.Key); } } @@ -126,8 +133,8 @@ namespace DotNetOpenAuth.Messaging.Reflection { get { List<string> values = new List<string>(this.message.ExtraData.Count + this.description.Mapping.Count); foreach (MessagePart part in this.description.Mapping.Values) { - if (part.GetValue(this.message) != null) { - values.Add(part.GetValue(this.message)); + if (part.GetValue(this.message, this.getOriginalValues) != null) { + values.Add(part.GetValue(this.message, this.getOriginalValues)); } } @@ -167,7 +174,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { // Never throw KeyNotFoundException for declared properties. - return part.GetValue(this.message); + return part.GetValue(this.message, this.getOriginalValues); } else { return this.message.ExtraData[key]; } @@ -223,7 +230,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <returns>True if the parameter by the given name has a set value. False otherwise.</returns> public bool ContainsKey(string key) { return this.message.ExtraData.ContainsKey(key) || - (this.description.Mapping.ContainsKey(key) && this.description.Mapping[key].GetValue(this.message) != null); + (this.description.Mapping.ContainsKey(key) && this.description.Mapping[key].GetValue(this.message, this.getOriginalValues) != null); } /// <summary> @@ -237,7 +244,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { } else { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { - if (part.GetValue(this.message) != null) { + if (part.GetValue(this.message, this.getOriginalValues) != null) { part.SetValue(this.message, null); return true; } @@ -255,7 +262,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { public bool TryGetValue(string key, out string value) { MessagePart part; if (this.description.Mapping.TryGetValue(key, out part)) { - value = part.GetValue(this.message); + value = part.GetValue(this.message, this.getOriginalValues); return value != null; } return this.message.ExtraData.TryGetValue(key, out value); @@ -306,7 +313,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { public bool Contains(KeyValuePair<string, string> item) { MessagePart part; if (this.description.Mapping.TryGetValue(item.Key, out part)) { - return string.Equals(part.GetValue(this.message), item.Value, StringComparison.Ordinal); + return string.Equals(part.GetValue(this.message, this.getOriginalValues), item.Value, StringComparison.Ordinal); } else { return this.message.ExtraData.Contains(item); } diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs index 08e2411..b876ec4 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs @@ -15,6 +15,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { using System.Net.Security; using System.Reflection; using System.Xml; + using DotNetOpenAuth.Configuration; using DotNetOpenAuth.OpenId; /// <summary> @@ -73,10 +74,10 @@ namespace DotNetOpenAuth.Messaging.Reflection { Contract.Assume(str != null); return bool.Parse(str); }; - Func<string, Identifier> safeIdentfier = str => { + Func<string, Identifier> safeIdentifier = str => { Contract.Assume(str != null); ErrorUtilities.VerifyFormat(str.Length > 0, MessagingStrings.NonEmptyStringExpected); - return Identifier.Parse(str); + return Identifier.Parse(str, true); }; Func<byte[], string> safeFromByteArray = bytes => { Contract.Assume(bytes != null); @@ -90,14 +91,14 @@ namespace DotNetOpenAuth.Messaging.Reflection { Contract.Assume(str != null); return new Realm(str); }; - Map<Uri>(uri => uri.AbsoluteUri, safeUri); - Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc)); - Map<byte[]>(safeFromByteArray, safeToByteArray); - Map<Realm>(realm => realm.ToString(), safeRealm); - Map<Identifier>(id => id.ToString(), safeIdentfier); - Map<bool>(value => value.ToString().ToLowerInvariant(), safeBool); - Map<CultureInfo>(c => c.Name, str => new CultureInfo(str)); - Map<CultureInfo[]>(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), str => str.Split(',').Select(s => new CultureInfo(s)).ToArray()); + Map<Uri>(uri => uri.AbsoluteUri, uri => uri.OriginalString, safeUri); + Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), null, str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc)); + Map<byte[]>(safeFromByteArray, null, safeToByteArray); + Map<Realm>(realm => realm.ToString(), realm => realm.OriginalString, safeRealm); + Map<Identifier>(id => id.SerializedString, id => id.OriginalString, safeIdentifier); + Map<bool>(value => value.ToString().ToLowerInvariant(), null, safeBool); + Map<CultureInfo>(c => c.Name, null, str => new CultureInfo(str)); + Map<CultureInfo[]>(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), null, str => str.Split(',').Select(s => new CultureInfo(s)).ToArray()); } /// <summary> @@ -129,9 +130,28 @@ namespace DotNetOpenAuth.Messaging.Reflection { Contract.Assume(this.memberDeclaredType != null); // CC missing PropertyInfo.PropertyType ensures result != null if (attribute.Encoder == null) { if (!converters.TryGetValue(this.memberDeclaredType, out this.converter)) { - this.converter = new ValueMapping( - obj => obj != null ? obj.ToString() : null, - str => str != null ? Convert.ChangeType(str, this.memberDeclaredType, CultureInfo.InvariantCulture) : null); + if (this.memberDeclaredType.IsGenericType && + this.memberDeclaredType.GetGenericTypeDefinition() == typeof(Nullable<>)) { + // It's a nullable type. Try again to look up an appropriate converter for the underlying type. + Type underlyingType = Nullable.GetUnderlyingType(this.memberDeclaredType); + ValueMapping underlyingMapping; + if (converters.TryGetValue(underlyingType, out underlyingMapping)) { + this.converter = new ValueMapping( + underlyingMapping.ValueToString, + null, + str => str != null ? underlyingMapping.StringToValue(str) : null); + } else { + this.converter = new ValueMapping( + obj => obj != null ? obj.ToString() : null, + null, + str => str != null ? Convert.ChangeType(str, underlyingType, CultureInfo.InvariantCulture) : null); + } + } else { + this.converter = new ValueMapping( + obj => obj != null ? obj.ToString() : null, + null, + str => str != null ? Convert.ChangeType(str, this.memberDeclaredType, CultureInfo.InvariantCulture) : null); + } } } else { this.converter = new ValueMapping(GetEncoder(attribute.Encoder)); @@ -189,7 +209,8 @@ namespace DotNetOpenAuth.Messaging.Reflection { try { if (this.IsConstantValue) { string constantValue = this.GetValue(message); - if (!string.Equals(constantValue, value)) { + var caseSensitivity = DotNetOpenAuthSection.Configuration.Messaging.Strict ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + if (!string.Equals(constantValue, value, caseSensitivity)) { throw new ArgumentException(string.Format( CultureInfo.CurrentCulture, MessagingStrings.UnexpectedMessagePartValueForConstant, @@ -211,7 +232,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { } /// <summary> - /// Gets the value of a member of a given message. + /// Gets the normalized form of a value of a member of a given message. /// Used in serialization. /// </summary> /// <param name="message">The message instance to read the value from.</param> @@ -219,7 +240,23 @@ namespace DotNetOpenAuth.Messaging.Reflection { internal string GetValue(IMessage message) { try { object value = this.GetValueAsObject(message); - return this.ToString(value); + return this.ToString(value, false); + } catch (FormatException ex) { + throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name); + } + } + + /// <summary> + /// Gets the value of a member of a given message. + /// Used in serialization. + /// </summary> + /// <param name="message">The message instance to read the value from.</param> + /// <param name="originalValue">A value indicating whether the original value should be retrieved (as opposed to a normalized form of it).</param> + /// <returns>The string representation of the member's value.</returns> + internal string GetValue(IMessage message, bool originalValue) { + try { + object value = this.GetValueAsObject(message); + return this.ToString(value, originalValue); } catch (FormatException ex) { throw ErrorUtilities.Wrap(ex, MessagingStrings.MessagePartWriteFailure, message.GetType(), this.Name); } @@ -252,15 +289,24 @@ namespace DotNetOpenAuth.Messaging.Reflection { } /// <summary> - /// Adds a pair of type conversion functions to the static converstion map. + /// Adds a pair of type conversion functions to the static conversion map. /// </summary> /// <typeparam name="T">The custom type to convert to and from strings.</typeparam> /// <param name="toString">The function to convert the custom type to a string.</param> + /// <param name="toOriginalString">The mapping function that converts some custom value to its original (non-normalized) string. May be null if the same as the <paramref name="toString"/> function.</param> /// <param name="toValue">The function to convert a string to the custom type.</param> - private static void Map<T>(Func<T, string> toString, Func<string, T> toValue) { + private static void Map<T>(Func<T, string> toString, Func<T, string> toOriginalString, Func<string, T> toValue) { + Contract.Requires<ArgumentNullException>(toString != null, "toString"); + Contract.Requires<ArgumentNullException>(toValue != null, "toValue"); + + if (toOriginalString == null) { + toOriginalString = toString; + } + Func<object, string> safeToString = obj => obj != null ? toString((T)obj) : null; + Func<object, string> safeToOriginalString = obj => obj != null ? toOriginalString((T)obj) : null; Func<string, object> safeToT = str => str != null ? toValue(str) : default(T); - converters.Add(typeof(T), new ValueMapping(safeToString, safeToT)); + converters.Add(typeof(T), new ValueMapping(safeToString, safeToOriginalString, safeToT)); } /// <summary> @@ -312,11 +358,12 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// Converts the member's value to its string representation. /// </summary> /// <param name="value">The value of the member.</param> + /// <param name="originalString">A value indicating whether a string matching the originally decoded string should be returned (as opposed to a normalized string).</param> /// <returns> /// The string representation of the member's value. /// </returns> - private string ToString(object value) { - return this.converter.ValueToString(value); + private string ToString(object value, bool originalString) { + return originalString ? this.converter.ValueToOriginalString(value) : this.converter.ValueToString(value); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs b/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs index 1c7631e..b0b8b47 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs @@ -19,6 +19,12 @@ namespace DotNetOpenAuth.Messaging.Reflection { internal readonly Func<object, string> ValueToString; /// <summary> + /// The mapping function that converts some custom type to the original string + /// (possibly non-normalized) that represents it. + /// </summary> + internal readonly Func<object, string> ValueToOriginalString; + + /// <summary> /// The mapping function that converts a string to some custom type. /// </summary> internal readonly Func<string, object> StringToValue; @@ -26,13 +32,15 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <summary> /// Initializes a new instance of the <see cref="ValueMapping"/> struct. /// </summary> - /// <param name="toString">The mapping function that converts some custom type to a string.</param> - /// <param name="toValue">The mapping function that converts a string to some custom type.</param> - internal ValueMapping(Func<object, string> toString, Func<string, object> toValue) { + /// <param name="toString">The mapping function that converts some custom value to a string.</param> + /// <param name="toOriginalString">The mapping function that converts some custom value to its original (non-normalized) string. May be null if the same as the <paramref name="toString"/> function.</param> + /// <param name="toValue">The mapping function that converts a string to some custom value.</param> + internal ValueMapping(Func<object, string> toString, Func<object, string> toOriginalString, Func<string, object> toValue) { Contract.Requires<ArgumentNullException>(toString != null); Contract.Requires<ArgumentNullException>(toValue != null); this.ValueToString = toString; + this.ValueToOriginalString = toOriginalString ?? toString; this.StringToValue = toValue; } @@ -45,8 +53,15 @@ namespace DotNetOpenAuth.Messaging.Reflection { var nullEncoder = encoder as IMessagePartNullEncoder; string nullString = nullEncoder != null ? nullEncoder.EncodedNullValue : null; + var originalStringEncoder = encoder as IMessagePartOriginalEncoder; + Func<object, string> originalStringEncode = encoder.Encode; + if (originalStringEncoder != null) { + originalStringEncode = originalStringEncoder.EncodeAsOriginalString; + } + this.ValueToString = obj => (obj != null) ? encoder.Encode(obj) : nullString; this.StringToValue = str => (str != null) ? encoder.Decode(str) : null; + this.ValueToOriginalString = obj => (obj != null) ? originalStringEncode(obj) : nullString; } } } diff --git a/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset b/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset new file mode 100644 index 0000000..db238b6 --- /dev/null +++ b/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<RuleSet Name="Migrated rules for DotNetOpenAuth.ruleset" Description="This rule set was created from the CodeAnalysisRules property for the "Debug (Any CPU)" configuration in project "C:\Users\andarno\git\dotnetopenid\src\DotNetOpenAuth\DotNetOpenAuth.csproj"." ToolsVersion="10.0"> + <IncludeAll Action="Warning" /> + <Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed"> + <Rule Id="CA1054" Action="None" /> + <Rule Id="CA1055" Action="None" /> + <Rule Id="CA1056" Action="None" /> + <Rule Id="CA2104" Action="None" /> + </Rules> +</RuleSet>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs b/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs new file mode 100644 index 0000000..4b88d04 --- /dev/null +++ b/src/DotNetOpenAuth/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/Mvc/OpenIdHelper.cs b/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs new file mode 100644 index 0000000..193e445 --- /dev/null +++ b/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------- +// <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.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> + /// <param name="page">The page being rendered.</param> + /// <returns>HTML that should be sent directly to the browser.</returns> + public static string OpenIdSelectorStyles(this HtmlHelper html, Page page) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Ensures(Contract.Result<string>() != null); + + StringWriter result = new StringWriter(); + result.WriteStylesheetLink(page, OpenId.RelyingParty.OpenIdSelector.EmbeddedStylesheetResourceName); + result.WriteStylesheetLink(page, 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> + /// <param name="page">The page being rendered.</param> + /// <returns>HTML that should be sent directly to the browser.</returns> + public static string OpenIdSelectorScripts(this HtmlHelper html, Page page) { + return OpenIdSelectorScripts(html, page, 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="page">The page being rendered.</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> + public static string OpenIdSelectorScripts(this HtmlHelper html, Page page, OpenIdSelector selectorOptions, OpenIdAjaxOptions additionalOptions) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Ensures(Contract.Result<string>() != null); + + if (selectorOptions == null) { + selectorOptions = new OpenId.RelyingParty.OpenIdSelector(); + } + + if (additionalOptions == null) { + additionalOptions = new OpenIdAjaxOptions(); + } + + StringWriter result = new StringWriter(); + + 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(page, scriptResources); + + if (selectorOptions.DownloadYahooUILibrary) { + result.WriteScriptTags(new[] { "https://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js" }); + } + + var blockBuilder = new StringWriter(); + 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(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenIdTextBox.EmbeddedLogoResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.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(page, OpenId.RelyingParty.OpenIdSelector.EmbeddedScriptResourceName); + + Reporting.RecordFeatureUse("MVC " + typeof(OpenIdSelector).Name); + return result.ToString(); + } + + /// <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="page">The page being rendered.</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, Page page, Identifier providerIdentifier, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(providerIdentifier != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + return OpenIdSelectorButton(html, page, 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="page">The page being rendered.</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, Page page, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + return OpenIdSelectorButton(html, page, "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="page">The page being rendered.</param> + /// <param name="buttons">The buttons to include on the selector.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelector(this HtmlHelper html, Page page, params SelectorButton[] buttons) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(buttons != null); + Contract.Ensures(Contract.Result<string>() != null); + + var writer = new StringWriter(); + 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, page, op.OPIdentifier, op.Image)); + continue; + } + + var openid = button as SelectorOpenIdButton; + if (openid != null) { + h.Write(OpenIdSelectorOpenIdButton(html, page, 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> + 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="page">The page being rendered.</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> + private static string OpenIdSelectorButton(this HtmlHelper html, Page page, string id, string cssClass, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(id != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + var writer = new StringWriter(); + 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, page.ClientScript.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 WriteScriptTags(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="page">The page being rendered.</param> + /// <param name="resourceName">Name of the resource.</param> + private static void WriteScriptTags(this TextWriter writer, Page page, string resourceName) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName)); + + WriteScriptTags(writer, page, 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="page">The page being rendered.</param> + /// <param name="resourceNames">The resource names.</param> + private static void WriteScriptTags(this TextWriter writer, Page page, IEnumerable<string> resourceNames) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(resourceNames != null); + + writer.WriteScriptTags(resourceNames.Select(r => page.ClientScript.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="page">The page being rendered.</param> + /// <param name="resourceName">Name of the resource containing the CSS content.</param> + private static void WriteStylesheetLink(this TextWriter writer, Page page, string resourceName) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName)); + + WriteStylesheetLink(writer, page.ClientScript.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 WriteStylesheetLink(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/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index 036c19a..dc59b56 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System.Diagnostics.Contracts; using System.Globalization; using System.IO; + using System.Linq; using System.Net; using System.Net.Mime; using System.Text; @@ -88,7 +89,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> /// <param name="message">The message with data to encode.</param> /// <returns>A dictionary of name-value pairs with their strings encoded.</returns> - internal static IDictionary<string, string> GetUriEscapedParameters(MessageDictionary message) { + internal static IDictionary<string, string> GetUriEscapedParameters(IEnumerable<KeyValuePair<string, string>> message) { var encodedDictionary = new Dictionary<string, string>(); UriEscapeParameters(message, encodedDictionary); return encodedDictionary; @@ -210,6 +211,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { if ((transmissionMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) { httpRequest = this.InitializeRequestAsAuthHeader(request); } else if ((transmissionMethod & HttpDeliveryMethods.PostRequest) != 0) { + var requestMessageWithBinaryData = request as IMessageWithBinaryData; + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || !requestMessageWithBinaryData.SendAsMultipart, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader); httpRequest = this.InitializeRequestAsPost(request); } else if ((transmissionMethod & HttpDeliveryMethods.GetRequest) != 0) { httpRequest = InitializeRequestAsGet(request); @@ -282,7 +285,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> /// <param name="source">The dictionary with names and values to encode.</param> /// <param name="destination">The dictionary to add the encoded pairs to.</param> - private static void UriEscapeParameters(IDictionary<string, string> source, IDictionary<string, string> destination) { + private static void UriEscapeParameters(IEnumerable<KeyValuePair<string, string>> source, IDictionary<string, string> destination) { Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(destination != null); @@ -359,12 +362,22 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { if (hasEntity) { // WARNING: We only set up the request stream for the caller if there is // extra data. If there isn't any extra data, the caller must do this themselves. - if (requestMessage.ExtraData.Count > 0) { - SendParametersInEntity(httpRequest, requestMessage.ExtraData); + var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData; + if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) { + // Include the binary data in the multipart entity, and any standard text extra message data. + // The standard declared message parts are included in the authorization header. + var multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData); + multiPartFields.AddRange(requestMessage.ExtraData.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value))); + this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields); } else { - // We'll assume the content length is zero since the caller may not have - // anything. They're responsible to change it when the add the payload if they have one. - httpRequest.ContentLength = 0; + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); + if (requestMessage.ExtraData.Count > 0) { + this.SendParametersInEntity(httpRequest, requestMessage.ExtraData); + } else { + // We'll assume the content length is zero since the caller may not have + // anything. They're responsible to change it when the add the payload if they have one. + httpRequest.ContentLength = 0; + } } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs index 004e7d5..cf09036 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -10,6 +10,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System.Collections.Specialized; using System.Diagnostics.Contracts; using System.Globalization; + using System.Linq; using System.Text; using System.Web; using DotNetOpenAuth.Messaging; @@ -157,7 +158,26 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant()); - var encodedDictionary = OAuthChannel.GetUriEscapedParameters(messageDictionary); + // For multipart POST messages, only include the message parts that are NOT + // in the POST entity (those parts that may appear in an OAuth authorization header). + var encodedDictionary = new Dictionary<string, string>(); + IEnumerable<KeyValuePair<string, string>> partsToInclude = Enumerable.Empty<KeyValuePair<string, string>>(); + var binaryMessage = message as IMessageWithBinaryData; + if (binaryMessage != null && binaryMessage.SendAsMultipart) { + HttpDeliveryMethods authHeaderInUseFlags = HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest; + ErrorUtilities.VerifyProtocol((binaryMessage.HttpMethods & authHeaderInUseFlags) == authHeaderInUseFlags, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader); + + // Include the declared keys in the signature as those will be signable. + // Cache in local variable to avoid recalculating DeclaredKeys in the delegate. + ICollection<string> declaredKeys = messageDictionary.DeclaredKeys; + partsToInclude = messageDictionary.Where(pair => declaredKeys.Contains(pair.Key)); + } else { + partsToInclude = messageDictionary; + } + + foreach (var pair in OAuthChannel.GetUriEscapedParameters(partsToInclude)) { + encodedDictionary[pair.Key] = pair.Value; + } // An incoming message will already have included the query and form parameters // in the message dictionary, but an outgoing message COULD have SOME parameters @@ -238,6 +258,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>. /// </returns> protected virtual bool IsSignatureValid(ITamperResistantOAuthMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + string signature = this.GetSignature(message); return message.Signature == signature; } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs index 7b369c3..4ff52fd 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs @@ -15,8 +15,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { [ContractClassFor(typeof(SigningBindingElementBase))] internal abstract class SigningBindingElementBaseContract : SigningBindingElementBase { /// <summary> - /// Prevents a default instance of the SigningBindingElementBaseContract - /// class from being created. + /// Prevents a default instance of the SigningBindingElementBaseContract class from being created. /// </summary> private SigningBindingElementBaseContract() : base(string.Empty) { diff --git a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs index a189dcf..dddbe9e 100644 --- a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs +++ b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth.OAuth { using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; + using System.Linq; using System.Net; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -111,6 +112,27 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Prepares an authorized request that carries an HTTP multi-part POST, allowing for binary data. + /// </summary> + /// <param name="endpoint">The URL and method on the Service Provider to send the request to.</param> + /// <param name="accessToken">The access token that permits access to the protected resource.</param> + /// <param name="binaryData">Extra parameters to include in the message. Must not be null, but may be empty.</param> + /// <returns>The initialized WebRequest object.</returns> + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint endpoint, string accessToken, IEnumerable<MultipartPostPart> binaryData) { + Contract.Requires<ArgumentNullException>(endpoint != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(accessToken)); + Contract.Requires<ArgumentNullException>(binaryData != null); + + AccessProtectedResourceRequest message = this.CreateAuthorizingMessage(endpoint, accessToken); + foreach (MultipartPostPart part in binaryData) { + message.BinaryData.Add(part); + } + + HttpWebRequest wr = this.OAuthChannel.InitializeRequest(message); + return wr; + } + + /// <summary> /// Prepares an HTTP request that has OAuth authorization already attached to it. /// </summary> /// <param name="message">The OAuth authorization message to attach to the HTTP request.</param> diff --git a/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs b/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs index b60fda4..f3231f0 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth.Messages { using System; + using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using DotNetOpenAuth.Messaging; @@ -13,7 +14,12 @@ namespace DotNetOpenAuth.OAuth.Messages { /// A message attached to a request for protected resources that provides the necessary /// credentials to be granted access to those resources. /// </summary> - public class AccessProtectedResourceRequest : SignedMessageBase, ITokenContainingMessage { + public class AccessProtectedResourceRequest : SignedMessageBase, ITokenContainingMessage, IMessageWithBinaryData { + /// <summary> + /// A store for the binary data that is carried in the message. + /// </summary> + private List<MultipartPostPart> binaryData = new List<MultipartPostPart>(); + /// <summary> /// Initializes a new instance of the <see cref="AccessProtectedResourceRequest"/> class. /// </summary> @@ -43,5 +49,24 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </remarks> [MessagePart("oauth_token", IsRequired = true)] public string AccessToken { get; set; } + + #region IMessageWithBinaryData Members + + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + public IList<MultipartPostPart> BinaryData { + get { return this.binaryData; } + } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + public bool SendAsMultipart { + get { return this.HttpMethod == "POST" && this.BinaryData.Count > 0; } + } + + #endregion } } diff --git a/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs b/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs index 3593446..fb4c9a1 100644 --- a/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs +++ b/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.4918 +// Runtime Version:4.0.30104.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth.OAuth { // 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", "2.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class OAuthStrings { @@ -115,29 +115,38 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> - /// Looks up a localized string similar to Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.. + /// Looks up a localized string similar to This OAuth service provider requires OAuth consumers to implement OAuth {0}, but this consumer appears to only support {1}.. /// </summary> - internal static string OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface { + internal static string MinimumConsumerVersionRequirementNotMet { get { - return ResourceManager.GetString("OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface", resourceCulture); + return ResourceManager.GetString("MinimumConsumerVersionRequirementNotMet", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to The OpenID Relying Party's realm is not recognized as belonging to the OAuth Consumer identified by the consumer key given.. + /// Looks up a localized string similar to Cannot send OAuth message as multipart POST without an authorization HTTP header because sensitive data would not be signed.. /// </summary> - internal static string OpenIdOAuthRealmConsumerKeyDoNotMatch { + internal static string MultipartPostMustBeUsedWithAuthHeader { get { - return ResourceManager.GetString("OpenIdOAuthRealmConsumerKeyDoNotMatch", resourceCulture); + return ResourceManager.GetString("MultipartPostMustBeUsedWithAuthHeader", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to This OAuth service provider requires OAuth consumers to implement OAuth {0}, but this consumer appears to only support {1}.. + /// Looks up a localized string similar to Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.. /// </summary> - internal static string MinimumConsumerVersionRequirementNotMet { + internal static string OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface { get { - return ResourceManager.GetString("MinimumConsumerVersionRequirementNotMet", resourceCulture); + return ResourceManager.GetString("OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The OpenID Relying Party's realm is not recognized as belonging to the OAuth Consumer identified by the consumer key given.. + /// </summary> + internal static string OpenIdOAuthRealmConsumerKeyDoNotMatch { + get { + return ResourceManager.GetString("OpenIdOAuthRealmConsumerKeyDoNotMatch", resourceCulture); } } diff --git a/src/DotNetOpenAuth/OAuth/OAuthStrings.resx b/src/DotNetOpenAuth/OAuth/OAuthStrings.resx index bbeeda9..34b314b 100644 --- a/src/DotNetOpenAuth/OAuth/OAuthStrings.resx +++ b/src/DotNetOpenAuth/OAuth/OAuthStrings.resx @@ -138,6 +138,9 @@ <data name="MinimumConsumerVersionRequirementNotMet" xml:space="preserve"> <value>This OAuth service provider requires OAuth consumers to implement OAuth {0}, but this consumer appears to only support {1}.</value> </data> + <data name="MultipartPostMustBeUsedWithAuthHeader" xml:space="preserve"> + <value>Cannot send OAuth message as multipart POST without an authorization HTTP header because sensitive data would not be signed.</value> + </data> <data name="OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface" xml:space="preserve"> <value>Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.</value> </data> diff --git a/src/DotNetOpenAuth/OpenId/Association.cs b/src/DotNetOpenAuth/OpenId/Association.cs index 62e91ec..3c7e89f 100644 --- a/src/DotNetOpenAuth/OpenId/Association.cs +++ b/src/DotNetOpenAuth/OpenId/Association.cs @@ -238,24 +238,28 @@ namespace DotNetOpenAuth.OpenId { /// </returns> public override int GetHashCode() { HMACSHA1 hmac = new HMACSHA1(this.SecretKey); - CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); + try { + CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); - byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); + byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); - cs.Write(hbytes, 0, hbytes.Length); - cs.Close(); + cs.Write(hbytes, 0, hbytes.Length); + cs.Close(); - byte[] hash = hmac.Hash; - hmac.Clear(); + byte[] hash = hmac.Hash; + hmac.Clear(); - long val = 0; - for (int i = 0; i < hash.Length; i++) { - val = val ^ (long)hash[i]; - } + long val = 0; + for (int i = 0; i < hash.Length; i++) { + val = val ^ (long)hash[i]; + } - val = val ^ this.Expires.ToFileTimeUtc(); + val = val ^ this.Expires.ToFileTimeUtc(); - return (int)val; + return (int)val; + } finally { + ((IDisposable)hmac).Dispose(); + } } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/BehaviorStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/Behaviors/BehaviorStrings.Designer.cs index 937ecaf..8c952ab 100644 --- a/src/DotNetOpenAuth/OpenId/Behaviors/BehaviorStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/Behaviors/BehaviorStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.4918 +// Runtime Version:4.0.30104.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth.OpenId.Behaviors { // 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", "2.0.0.0")] + [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 { diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs index 43d6c03..370192a 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs @@ -209,7 +209,8 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// or if unsolicited assertions should be rejected at the RP; otherwise <c>false</c>. /// </returns> private bool UseRequestNonce(IMessage message) { - return message != null && (message.Version.Major < 2 || this.securitySettings.RejectUnsolicitedAssertions); + return message != null && (this.securitySettings.RejectUnsolicitedAssertions || + (message.Version.Major < 2 && this.securitySettings.ProtectDownlevelReplayAttacks)); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs b/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs index d27619f..1b58c2f 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs @@ -51,7 +51,7 @@ namespace DotNetOpenAuth.OpenId.Extensions { return; } - if (req.Provider.IsExtensionSupported<ClaimsRequest>()) { + if (req.DiscoveryResult.IsExtensionSupported<ClaimsRequest>()) { Logger.OpenId.Debug("Skipping generation of AX request because the Identifier advertises the Provider supports the Sreg extension."); return; } @@ -277,8 +277,7 @@ namespace DotNetOpenAuth.OpenId.Extensions { /// <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); - var provider = (RelyingParty.ServiceEndpoint)request.Provider; - attributeFormat = DetectAXFormat(provider.ProviderDescription.Capabilities); + attributeFormat = DetectAXFormat(request.DiscoveryResult.Capabilities); return attributeFormat != AXAttributeFormats.None; } diff --git a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs index ae483a6..55c2dc5 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs @@ -28,8 +28,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { /// <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="IProviderEndpoint.IsExtensionSupported"/> method on the <see cref="DotNetOpenAuth.OpenId.RelyingParty.IAuthenticationRequest.Provider"/> - /// object.</para> + /// <see cref="IdentifierDiscoveryResult.IsExtensionSupported<T>()"/> method.</para> /// </remarks> [Serializable] public sealed class UIRequest : IOpenIdMessageExtension, IMessageWithEvents { @@ -63,6 +62,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { /// </summary> public UIRequest() { this.LanguagePreference = new[] { CultureInfo.CurrentUICulture }; + this.Mode = UIModes.Popup; } /// <summary> @@ -77,12 +77,11 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { public CultureInfo[] LanguagePreference { get; set; } /// <summary> - /// Gets the style of UI that the RP is hosting the OP's authentication page in. + /// 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> - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Design is to allow this later to be changable when more than one value exists.")] [MessagePart("mode", AllowEmpty = false, IsRequired = true)] - public string Mode { get { return UIModes.Popup; } } + public string Mode { get; set; } /// <summary> /// Gets or sets a value indicating whether the Relying Party has an icon diff --git a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs new file mode 100644 index 0000000..ba9852e --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs @@ -0,0 +1,515 @@ +//----------------------------------------------------------------------- +// <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> + 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, + "Unrecognized or missing canonicalization method."); + ErrorUtilities.VerifyProtocol( + signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null, + "Unrecognized or missing signature method."); + 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), "X.509 signing certificate issued to {0}, but a certificate for {1} was expected.", 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), "Invalid XmlDSig signature on XRDS document."); + } + + /// <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.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/IIdentifierDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/IIdentifierDiscoveryService.cs new file mode 100644 index 0000000..eb2bf98 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/IIdentifierDiscoveryService.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// <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 class IIdentifierDiscoveryServiceContract : IIdentifierDiscoveryService { + #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/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs index 831dfa9..36ec784 100644 --- a/src/DotNetOpenAuth/OpenId/Identifier.cs +++ b/src/DotNetOpenAuth/OpenId/Identifier.cs @@ -39,6 +39,18 @@ namespace DotNetOpenAuth.OpenId { public 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> @@ -59,6 +71,18 @@ namespace DotNetOpenAuth.OpenId { 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> @@ -118,11 +142,32 @@ namespace DotNetOpenAuth.OpenId { 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)) { - return new XriIdentifier(identifier); + id = new XriIdentifier(identifier); } else { - return new UriIdentifier(identifier); + id = new UriIdentifier(identifier); } + + id.IsDeserializedInstance = serializeExactValue; + return id; } /// <summary> @@ -212,14 +257,17 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Performs discovery on the Identifier. + /// 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="requestHandler">The web request handler to use for discovery.</param> - /// <returns> - /// An initialized structure containing the discovered provider endpoint information. - /// </returns> - [Pure] - internal abstract IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler); + /// <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. diff --git a/src/DotNetOpenAuth/OpenId/IdentifierContract.cs b/src/DotNetOpenAuth/OpenId/IdentifierContract.cs index 498ae45..4af18e1 100644 --- a/src/DotNetOpenAuth/OpenId/IdentifierContract.cs +++ b/src/DotNetOpenAuth/OpenId/IdentifierContract.cs @@ -24,19 +24,6 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Performs discovery on the Identifier. - /// </summary> - /// <param name="requestHandler">The web request handler to use for discovery.</param> - /// <returns> - /// An initialized structure containing the discovered provider endpoint information. - /// </returns> - internal override IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler) { - Contract.Requires<ArgumentNullException>(requestHandler != null); - Contract.Ensures(Contract.Result<IEnumerable<ServiceEndpoint>>() != null); - throw new NotImplementedException(); - } - - /// <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. diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs b/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs index f8744d0..c851f24 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs @@ -1,13 +1,15 @@ //----------------------------------------------------------------------- -// <copyright file="ServiceEndpoint.cs" company="Andrew Arnott"> +// <copyright file="IdentifierDiscoveryResult.cs" company="Andrew Arnott"> // Copyright (c) Andrew Arnott. All rights reserved. // </copyright> //----------------------------------------------------------------------- -namespace DotNetOpenAuth.OpenId.RelyingParty { +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; @@ -15,17 +17,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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}")] - internal class ServiceEndpoint : IXrdsProviderEndpoint { + public sealed class IdentifierDiscoveryResult : IProviderEndpoint { /// <summary> - /// The i-name identifier the user actually typed in - /// or the url identifier with the scheme stripped off. + /// Backing field for the <see cref="Protocol"/> property. /// </summary> - private string friendlyIdentifierForDisplay; + private Protocol protocol; /// <summary> /// Backing field for the <see cref="ClaimedIdentifier"/> property. @@ -33,23 +35,12 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private Identifier claimedIdentifier; /// <summary> - /// The OpenID protocol version used at the identity Provider. - /// </summary> - private Protocol protocol; - - /// <summary> - /// The @priority given in the XRDS document for this specific OP endpoint. - /// </summary> - private int? uriPriority; - - /// <summary> - /// The @priority given in the XRDS document for this service - /// (which may consist of several endpoints). + /// Backing field for the <see cref="FriendlyIdentifierForDisplay"/> property. /// </summary> - private int? servicePriority; + private string friendlyIdentifierForDisplay; /// <summary> - /// Initializes a new instance of the <see cref="ServiceEndpoint"/> class. + /// 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> @@ -57,47 +48,23 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <param name="providerLocalIdentifier">The Provider Local Identifier.</param> /// <param name="servicePriority">The service priority.</param> /// <param name="uriPriority">The URI priority.</param> - private ServiceEndpoint(ProviderEndpointDescription providerEndpoint, Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier, int? servicePriority, int? uriPriority) { - Contract.Requires<ArgumentNullException>(claimedIdentifier != null); - Contract.Requires<ArgumentNullException>(providerEndpoint != null); - this.ProviderDescription = providerEndpoint; - this.ClaimedIdentifier = claimedIdentifier; - this.UserSuppliedIdentifier = userSuppliedIdentifier; - this.ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier; - this.servicePriority = servicePriority; - this.uriPriority = uriPriority; - } - - /// <summary> - /// Initializes a new instance of the <see cref="ServiceEndpoint"/> 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="protocol">The protocol.</param> - /// <remarks> - /// Used for deserializing <see cref="ServiceEndpoint"/> from authentication responses. - /// </remarks> - private ServiceEndpoint(Uri providerEndpoint, Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier, Protocol protocol) { + private IdentifierDiscoveryResult(ProviderEndpointDescription providerEndpoint, Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier, int? servicePriority, int? uriPriority) { Contract.Requires<ArgumentNullException>(providerEndpoint != null); Contract.Requires<ArgumentNullException>(claimedIdentifier != null); - Contract.Requires<ArgumentNullException>(providerLocalIdentifier != null); - Contract.Requires<ArgumentNullException>(protocol != null); - + this.ProviderEndpoint = providerEndpoint.Uri; + this.Capabilities = new ReadOnlyCollection<string>(providerEndpoint.Capabilities); + this.Version = providerEndpoint.Version; this.ClaimedIdentifier = claimedIdentifier; - this.UserSuppliedIdentifier = userSuppliedIdentifier; - this.ProviderDescription = new ProviderEndpointDescription(providerEndpoint, protocol.Version); this.ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier; - this.protocol = protocol; + this.UserSuppliedIdentifier = userSuppliedIdentifier; + this.ServicePriority = servicePriority; + this.ProviderEndpointPriority = uriPriority; } /// <summary> - /// Gets the URL that the OpenID Provider receives authentication requests at. + /// Gets the detected version of OpenID implemented by the Provider. /// </summary> - Uri IProviderEndpoint.Uri { - get { return this.ProviderDescription.Endpoint; } - } + public Version Version { get; private set; } /// <summary> /// Gets the Identifier that was presented by the end user to the Relying Party, @@ -110,19 +77,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public Identifier UserSuppliedIdentifier { get; private set; } /// <summary> - /// Gets or sets the Identifier that the end user claims to own. + /// Gets the Identifier that the end user claims to control. /// </summary> public Identifier ClaimedIdentifier { get { return this.claimedIdentifier; } - set { + 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.Parse(value) : null; + this.claimedIdentifier = value != null ? Identifier.Reparse(value) : null; } } @@ -134,8 +101,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public Identifier ProviderLocalIdentifier { get; private set; } /// <summary> - /// Gets the value for the <see cref="IAuthenticationResponse.FriendlyIdentifierForDisplay"/> property. + /// 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) { @@ -149,8 +117,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } else if (uri != null) { if (uri != this.Protocol.ClaimedIdentifierForOPIdentifier) { - string displayUri = uri.Uri.Host + uri.Uri.AbsolutePath; - displayUri = displayUri.TrimEnd('/'); + 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 @@ -162,57 +137,45 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.friendlyIdentifierForDisplay = this.ClaimedIdentifier; } } + return this.friendlyIdentifierForDisplay; } } /// <summary> - /// Gets the list of services available at this OP Endpoint for the - /// claimed Identifier. May be null. + /// Gets the provider endpoint. /// </summary> - public ReadOnlyCollection<string> ProviderSupportedServiceTypeUris { - get { return this.ProviderDescription.Capabilities; } - } + public Uri ProviderEndpoint { get; private set; } /// <summary> - /// Gets the OpenID protocol used by the Provider. + /// Gets the @priority given in the XRDS document for this specific OP endpoint. /// </summary> - public Protocol Protocol { - get { - if (this.protocol == null) { - this.protocol = Protocol.Lookup(this.ProviderDescription.ProtocolVersion); - } - - return this.protocol; - } - } - - #region IXrdsProviderEndpoint Members + public int? ProviderEndpointPriority { get; private set; } /// <summary> - /// Gets the priority associated with this service that may have been given - /// in the XRDS document. + /// Gets the @priority given in the XRDS document for this service + /// (which may consist of several endpoints). /// </summary> - int? IXrdsProviderEndpoint.ServicePriority { - get { return this.servicePriority; } - } + public int? ServicePriority { get; private set; } /// <summary> - /// Gets the priority associated with the service endpoint URL. + /// Gets the collection of service type URIs found in the XRDS document describing this Provider. /// </summary> - int? IXrdsProviderEndpoint.UriPriority { - get { return this.uriPriority; } - } + /// <value>Should never be null, but may be empty.</value> + public ReadOnlyCollection<string> Capabilities { get; private set; } - #endregion + #region IProviderEndpoint Members /// <summary> - /// Gets the detected version of OpenID implemented by the Provider. + /// Gets the URL that the OpenID Provider receives authentication requests at. /// </summary> - Version IProviderEndpoint.Version { - get { return this.ProviderDescription.ProtocolVersion; } + /// <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. @@ -220,7 +183,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <remarks> /// Endpoints lacking any priority value are sorted to the end of the list. /// </remarks> - internal static Comparison<IXrdsProviderEndpoint> EndpointOrder { + 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 @@ -234,11 +197,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (result != 0) { return result; } - if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) { - return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value); - } else if (se1.UriPriority.HasValue) { + 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.UriPriority.HasValue) { + } else if (se2.ProviderEndpointPriority.HasValue) { return 1; } else { return 0; @@ -250,11 +213,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return 1; } else { // neither service defines a priority, so base ordering by uri priority. - if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) { - return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value); - } else if (se1.UriPriority.HasValue) { + 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.UriPriority.HasValue) { + } else if (se2.ProviderEndpointPriority.HasValue) { return 1; } else { return 0; @@ -266,35 +229,25 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Gets the URL which accepts OpenID Authentication protocol messages. + /// Gets the protocol used by the OpenID Provider. /// </summary> - /// <remarks> - /// Obtained by performing discovery on the User-Supplied Identifier. - /// This value MUST be an absolute HTTP or HTTPS URL. - /// </remarks> - internal Uri ProviderEndpoint { - get { return this.ProviderDescription.Endpoint; } - } + internal Protocol Protocol { + get { + if (this.protocol == null) { + this.protocol = Protocol.Lookup(this.Version); + } - /// <summary> - /// Gets a value indicating whether the <see cref="ProviderEndpoint"/> is using an encrypted channel. - /// </summary> - internal bool IsSecure { - get { return string.Equals(this.ProviderEndpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); } + return this.protocol; + } } /// <summary> - /// Gets the provider description. - /// </summary> - internal ProviderEndpointDescription ProviderDescription { get; private set; } - - /// <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 ==(ServiceEndpoint se1, ServiceEndpoint se2) { + public static bool operator ==(IdentifierDiscoveryResult se1, IdentifierDiscoveryResult se2) { return se1.EqualsNullSafe(se2); } @@ -304,44 +257,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <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 !=(ServiceEndpoint se1, ServiceEndpoint se2) { + public static bool operator !=(IdentifierDiscoveryResult se1, IdentifierDiscoveryResult se2) { return !(se1 == se2); } /// <summary> - /// Checks for the presence of a given Type URI in an XRDS service. - /// </summary> - /// <param name="typeUri">The type URI to check for.</param> - /// <returns> - /// <c>true</c> if the service type uri is present; <c>false</c> otherwise. - /// </returns> - public bool IsTypeUriPresent(string typeUri) { - return this.ProviderDescription.IsExtensionSupported(typeUri); - } - - /// <summary> - /// Determines whether a given extension is supported by this endpoint. - /// </summary> - /// <typeparam name="T">The type of extension to check support for on this endpoint.</typeparam> - /// <returns> - /// <c>true</c> if the extension is supported by this endpoint; otherwise, <c>false</c>. - /// </returns> - public bool IsExtensionSupported<T>() where T : IOpenIdMessageExtension, new() { - return this.ProviderDescription.IsExtensionSupported<T>(); - } - - /// <summary> - /// Determines whether a given extension is supported by this endpoint. - /// </summary> - /// <param name="extensionType">The type of extension to check support for on this endpoint.</param> - /// <returns> - /// <c>true</c> if the extension is supported by this endpoint; otherwise, <c>false</c>. - /// </returns> - public bool IsExtensionSupported(Type extensionType) { - return this.ProviderDescription.IsExtensionSupported(extensionType); - } - - /// <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> @@ -352,7 +272,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// The <paramref name="obj"/> parameter is null. /// </exception> public override bool Equals(object obj) { - ServiceEndpoint other = obj as ServiceEndpoint; + var other = obj as IdentifierDiscoveryResult; if (other == null) { return false; } @@ -388,57 +308,95 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { StringBuilder builder = new StringBuilder(); builder.AppendLine("ClaimedIdentifier: " + this.ClaimedIdentifier); builder.AppendLine("ProviderLocalIdentifier: " + this.ProviderLocalIdentifier); - builder.AppendLine("ProviderEndpoint: " + this.ProviderEndpoint.AbsoluteUri); - builder.AppendLine("OpenID version: " + this.Protocol.Version); + builder.AppendLine("ProviderEndpoint: " + this.ProviderEndpoint); + builder.AppendLine("OpenID version: " + this.Version); builder.AppendLine("Service Type URIs:"); - if (this.ProviderSupportedServiceTypeUris != null) { - foreach (string serviceTypeUri in this.ProviderSupportedServiceTypeUris) { - builder.Append("\t"); - builder.AppendLine(serviceTypeUri); - } - } else { - builder.AppendLine("\t(unavailable)"); + foreach (string serviceTypeUri in this.Capabilities) { + builder.Append("\t"); + builder.AppendLine(serviceTypeUri); } builder.Length -= Environment.NewLine.Length; // trim last newline return builder.ToString(); } /// <summary> - /// Reads previously discovered information about an endpoint - /// from a solicited authentication assertion for validation. + /// Checks whether the OpenId Identifier claims support for a given extension. /// </summary> - /// <param name="reader">The reader from which to deserialize the <see cref="ServiceEndpoint"/>.</param> + /// <typeparam name="T">The extension whose support is being queried.</typeparam> /// <returns> - /// A <see cref="ServiceEndpoint"/> object that has everything - /// except the <see cref="ProviderSupportedServiceTypeUris"/> - /// deserialized. + /// True if support for the extension is advertised. False otherwise. /// </returns> - internal static ServiceEndpoint Deserialize(TextReader reader) { - var claimedIdentifier = Identifier.Parse(reader.ReadLine()); - var providerLocalIdentifier = Identifier.Parse(reader.ReadLine()); - string userSuppliedIdentifier = reader.ReadLine(); - if (userSuppliedIdentifier.Length == 0) { - userSuppliedIdentifier = null; + /// <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; + } } - var providerEndpoint = new Uri(reader.ReadLine()); - var protocol = Protocol.FindBestVersion(p => p.Version, new[] { new Version(reader.ReadLine()) }); - return new ServiceEndpoint(providerEndpoint, claimedIdentifier, userSuppliedIdentifier, providerLocalIdentifier, protocol); + + return false; } /// <summary> - /// Creates a <see cref="ServiceEndpoint"/> instance to represent some OP Identifier. + /// 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="ServiceEndpoint"/> instance</returns> - internal static ServiceEndpoint CreateForProviderIdentifier(Identifier providerIdentifier, ProviderEndpointDescription providerEndpoint, int? servicePriority, int? uriPriority) { + /// <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.Detect(providerEndpoint.Capabilities); + Protocol protocol = Protocol.Lookup(providerEndpoint.Version); - return new ServiceEndpoint( + return new IdentifierDiscoveryResult( providerEndpoint, protocol.ClaimedIdentifierForOPIdentifier, providerIdentifier, @@ -448,20 +406,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Creates a <see cref="ServiceEndpoint"/> instance to represent some Claimed Identifier. + /// 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="ServiceEndpoint"/> instance</returns> - internal static ServiceEndpoint CreateForClaimedIdentifier(Identifier claimedIdentifier, Identifier providerLocalIdentifier, ProviderEndpointDescription providerEndpoint, int? servicePriority, int? uriPriority) { + /// <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="ServiceEndpoint"/> instance to represent some Claimed Identifier. + /// 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> @@ -469,24 +427,30 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <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="ServiceEndpoint"/> instance</returns> - internal static ServiceEndpoint CreateForClaimedIdentifier(Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier, ProviderEndpointDescription providerEndpoint, int? servicePriority, int? uriPriority) { - return new ServiceEndpoint(providerEndpoint, claimedIdentifier, userSuppliedIdentifier, providerLocalIdentifier, servicePriority, uriPriority); + /// <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> - /// Saves the discovered information about this endpoint - /// for later comparison to validate assertions. + /// Determines whether a given type URI is present on the specified provider endpoint. /// </summary> - /// <param name="writer">The writer to use for serializing out the fields.</param> - internal void Serialize(TextWriter writer) { - writer.WriteLine(this.ClaimedIdentifier); - writer.WriteLine(this.ProviderLocalIdentifier); - writer.WriteLine(this.UserSuppliedIdentifier); - writer.WriteLine(this.ProviderEndpoint); - writer.WriteLine(this.Protocol.Version); - - // No reason to serialize priority. We only needed priority to decide whether to use this endpoint. + /// <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> @@ -495,22 +459,39 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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(IXrdsProviderEndpoint endpoint) { + 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.IsTypeUriPresent(Protocol.V20.OPIdentifierServiceTypeURI)) { + if (endpoint.Capabilities.Contains(Protocol.V20.OPIdentifierServiceTypeURI)) { return 0; } - if (endpoint.IsTypeUriPresent(Protocol.V20.ClaimedIdentifierServiceTypeURI)) { + if (endpoint.Capabilities.Contains(Protocol.V20.ClaimedIdentifierServiceTypeURI)) { return 1; } - if (endpoint.IsTypeUriPresent(Protocol.V11.ClaimedIdentifierServiceTypeURI)) { + if (endpoint.Capabilities.Contains(Protocol.V11.ClaimedIdentifierServiceTypeURI)) { return 2; } - if (endpoint.IsTypeUriPresent(Protocol.V10.ClaimedIdentifierServiceTypeURI)) { + 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/Messages/AssociateRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs index 5215022..3fd9424 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/AssociateRequest.cs @@ -76,16 +76,16 @@ namespace DotNetOpenAuth.OpenId.Messages { /// Null if no association could be created that meet the security requirements /// and the provider OpenID version. /// </returns> - internal static AssociateRequest Create(SecuritySettings securityRequirements, ProviderEndpointDescription provider) { + 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.Endpoint.IsTransportSecure(); + bool unencryptedAllowed = provider.Uri.IsTransportSecure(); bool useDiffieHellman = !unencryptedAllowed; string associationType, sessionType; - if (!HmacShaAssociation.TryFindBestAssociation(Protocol.Lookup(provider.ProtocolVersion), true, securityRequirements, useDiffieHellman, out associationType, out 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; @@ -106,19 +106,19 @@ namespace DotNetOpenAuth.OpenId.Messages { /// Null if no association could be created that meet the security requirements /// and the provider OpenID version. /// </returns> - internal static AssociateRequest Create(SecuritySettings securityRequirements, ProviderEndpointDescription provider, string associationType, string sessionType) { + 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.Endpoint.IsTransportSecure(); + bool unencryptedAllowed = provider.Uri.IsTransportSecure(); if (unencryptedAllowed) { - var associateRequest = new AssociateUnencryptedRequest(provider.ProtocolVersion, provider.Endpoint); + var associateRequest = new AssociateUnencryptedRequest(provider.Version, provider.Uri); associateRequest.AssociationType = associationType; return associateRequest; } else { - var associateRequest = new AssociateDiffieHellmanRequest(provider.ProtocolVersion, provider.Endpoint); + var associateRequest = new AssociateDiffieHellmanRequest(provider.Version, provider.Uri); associateRequest.AssociationType = associationType; associateRequest.SessionType = sessionType; associateRequest.InitializeRequest(); diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs index 5306c54..db69d3d 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationRequest.cs @@ -45,7 +45,7 @@ namespace DotNetOpenAuth.OpenId.Messages { // Copy all message parts from the id_res message into this one, // except for the openid.mode parameter. - MessageDictionary checkPayload = channel.MessageDescriptions.GetAccessor(message); + 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)) { diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs index 61825e8..f1bb5ac 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckAuthenticationResponse.cs @@ -47,7 +47,7 @@ namespace DotNetOpenAuth.OpenId.Messages { // really doesn't exist. OpenID 2.0 section 11.4.2.2. IndirectSignedResponse signedResponse = new IndirectSignedResponse(request, provider.Channel); string invalidateHandle = ((ITamperResistantOpenIdMessage)signedResponse).InvalidateHandle; - if (invalidateHandle != null && provider.AssociationStore.GetAssociation(AssociationRelyingPartyType.Smart, invalidateHandle) == null) { + if (!string.IsNullOrEmpty(invalidateHandle) && provider.AssociationStore.GetAssociation(AssociationRelyingPartyType.Smart, invalidateHandle) == null) { this.InvalidateHandle = invalidateHandle; } } @@ -70,8 +70,10 @@ namespace DotNetOpenAuth.OpenId.Messages { /// <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 = false)] + [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/Messages/IndirectSignedResponse.cs b/src/DotNetOpenAuth/OpenId/Messages/IndirectSignedResponse.cs index 2f02974..fff4cf6 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/IndirectSignedResponse.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/IndirectSignedResponse.cs @@ -207,7 +207,11 @@ namespace DotNetOpenAuth.OpenId.Messages { /// 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> - [MessagePart("openid.invalidate_handle", IsRequired = false, AllowEmpty = false)] + /// <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> diff --git a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs index 636df67..1a6e7e9 100644 --- a/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/NoDiscoveryIdentifier.cs @@ -70,17 +70,6 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Performs discovery on the Identifier. - /// </summary> - /// <param name="requestHandler">The web request handler to use for discovery.</param> - /// <returns> - /// An initialized structure containing the discovered provider endpoint information. - /// </returns> - internal override IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler) { - return Enumerable.Empty<ServiceEndpoint>(); - } - - /// <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. diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index 33a16f8..43283ac 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.4927 +// Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth.OpenId { // 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", "2.0.0.0")] + [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 { @@ -196,6 +196,15 @@ namespace DotNetOpenAuth.OpenId { } /// <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 { @@ -416,6 +425,15 @@ namespace DotNetOpenAuth.OpenId { } /// <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 { @@ -722,6 +740,15 @@ namespace DotNetOpenAuth.OpenId { } /// <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 { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index c5f506d..fab03a9 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -346,4 +346,13 @@ Discovered endpoint info: <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> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs b/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs index 3e75e61..04eb23c 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdUtilities.cs @@ -16,13 +16,14 @@ namespace DotNetOpenAuth.OpenId { 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> - internal static class OpenIdUtilities { + public static class OpenIdUtilities { /// <summary> /// The prefix to designate this library's proprietary parameters added to the protocol. /// </summary> diff --git a/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs b/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs index 664a127..00468ed 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdXrdsHelper.cs @@ -27,6 +27,9 @@ namespace DotNetOpenAuth.OpenId { /// 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); @@ -39,6 +42,9 @@ namespace DotNetOpenAuth.OpenId { /// <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 @@ -55,15 +61,16 @@ namespace DotNetOpenAuth.OpenId { /// <returns> /// A sequence of OpenID Providers that can assert ownership of the <paramref name="claimedIdentifier"/>. /// </returns> - internal static IEnumerable<ServiceEndpoint> CreateServiceEndpoints(this XrdsDocument xrds, UriIdentifier claimedIdentifier, UriIdentifier userSuppliedIdentifier) { - var endpoints = new List<ServiceEndpoint>(); + 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)); - // If any OP Identifier service elements were found, we must not proceed - // to return any Claimed Identifier services. - if (endpoints.Count == 0) { - 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; @@ -76,18 +83,14 @@ namespace DotNetOpenAuth.OpenId { /// <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<ServiceEndpoint> CreateServiceEndpoints(this XrdsDocument xrds, XriIdentifier userSuppliedIdentifier) { + 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<ServiceEndpoint>>() != null); - var endpoints = new List<ServiceEndpoint>(); - endpoints.AddRange(xrds.GenerateOPIdentifierServiceEndpoints(userSuppliedIdentifier)); + Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null); - // If any OP Identifier service elements were found, we must not proceed - // to return any Claimed Identifier services. - if (endpoints.Count == 0) { - endpoints.AddRange(xrds.GenerateClaimedIdentifierServiceEndpoints(userSuppliedIdentifier)); - } + 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; @@ -99,15 +102,15 @@ namespace DotNetOpenAuth.OpenId { /// <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<ServiceEndpoint> GenerateOPIdentifierServiceEndpoints(this XrdsDocument xrds, Identifier opIdentifier) { + 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<ServiceEndpoint>>() != 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 ServiceEndpoint.CreateForProviderIdentifier(opIdentifier, providerDescription, service.Priority, uri.Priority); + select IdentifierDiscoveryResult.CreateForProviderIdentifier(opIdentifier, providerDescription, service.Priority, uri.Priority); } /// <summary> @@ -120,12 +123,16 @@ namespace DotNetOpenAuth.OpenId { /// <returns> /// A sequence of the providers that can assert ownership of the given identifier. /// </returns> - private static IEnumerable<ServiceEndpoint> GenerateClaimedIdentifierServiceEndpoints(this XrdsDocument xrds, UriIdentifier claimedIdentifier, UriIdentifier userSuppliedIdentifier) { + 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 ServiceEndpoint.CreateForClaimedIdentifier(claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier, providerEndpoint, service.Priority, uri.Priority); + select IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier, providerEndpoint, service.Priority, uri.Priority); } /// <summary> @@ -135,7 +142,12 @@ namespace DotNetOpenAuth.OpenId { /// <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<ServiceEndpoint> GenerateClaimedIdentifierServiceEndpoints(this XrdsDocument xrds, XriIdentifier userSuppliedIdentifier) { + 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 @@ -148,7 +160,7 @@ namespace DotNetOpenAuth.OpenId { // 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 ServiceEndpoint.CreateForClaimedIdentifier(claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier, providerEndpoint, service.Priority, uri.Priority); + yield return IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier, providerEndpoint, service.Priority, uri.Priority); } } } @@ -158,8 +170,11 @@ namespace DotNetOpenAuth.OpenId { /// </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 XrdsDocument xrds) { - return from xrd in xrds.XrdElements + 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; } @@ -170,8 +185,11 @@ namespace DotNetOpenAuth.OpenId { /// </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 XrdsDocument xrds) { - return from xrd in xrds.XrdElements + 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; } @@ -183,6 +201,9 @@ namespace DotNetOpenAuth.OpenId { /// <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/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs index 827ca6c..10c69a3 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs @@ -20,10 +20,12 @@ namespace DotNetOpenAuth.OpenId.Provider { 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> @@ -43,6 +45,12 @@ namespace DotNetOpenAuth.OpenId.Provider { 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() @@ -160,6 +168,13 @@ namespace DotNetOpenAuth.OpenId.Provider { } /// <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 association store. /// </summary> internal IAssociationStore<AssociationRelyingPartyType> AssociationStore { get; private set; } @@ -173,6 +188,25 @@ namespace DotNetOpenAuth.OpenId.Provider { } /// <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> @@ -314,7 +348,7 @@ namespace DotNetOpenAuth.OpenId.Provider { /// 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="relyingParty">The URL of the Relying Party web site. + /// <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 @@ -323,15 +357,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// <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 relyingParty, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { + 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>(relyingParty != null); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); Contract.Requires<ArgumentNullException>(claimedIdentifier != null); Contract.Requires<ArgumentNullException>(localIdentifier != null); - this.PrepareUnsolicitedAssertion(providerEndpoint, relyingParty, claimedIdentifier, localIdentifier, extensions).Send(); + this.PrepareUnsolicitedAssertion(providerEndpoint, relyingPartyRealm, claimedIdentifier, localIdentifier, extensions).Send(); } /// <summary> @@ -340,7 +374,7 @@ namespace DotNetOpenAuth.OpenId.Provider { /// 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="relyingParty">The URL of the Relying Party web site. + /// <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 @@ -353,10 +387,10 @@ namespace DotNetOpenAuth.OpenId.Provider { /// 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 relyingParty, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { + 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>(relyingParty != null); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); Contract.Requires<ArgumentNullException>(claimedIdentifier != null); Contract.Requires<ArgumentNullException>(localIdentifier != null); Contract.Requires<InvalidOperationException>(this.Channel.WebRequestHandler != null); @@ -366,8 +400,8 @@ namespace DotNetOpenAuth.OpenId.Provider { // 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 = DotNetOpenAuth.OpenId.RelyingParty.ServiceEndpoint.CreateForClaimedIdentifier(claimedIdentifier, localIdentifier, new ProviderEndpointDescription(providerEndpoint, Protocol.Default.Version), null, null); - var discoveredEndpoints = claimedIdentifier.Discover(this.WebRequestHandler); + 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}", @@ -385,11 +419,11 @@ namespace DotNetOpenAuth.OpenId.Provider { Logger.OpenId.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier); RelyingPartyEndpointDescription returnToEndpoint = null; - var returnToEndpoints = relyingParty.DiscoverReturnToEndpoints(this.WebRequestHandler, true); + var returnToEndpoints = relyingPartyRealm.DiscoverReturnToEndpoints(this.WebRequestHandler, true); if (returnToEndpoints != null) { returnToEndpoint = returnToEndpoints.FirstOrDefault(); } - ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingParty); + ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingPartyRealm); var positiveAssertion = new PositiveAssertionResponse(returnToEndpoint) { ProviderEndpoint = providerEndpoint, @@ -428,6 +462,10 @@ namespace DotNetOpenAuth.OpenId.Provider { if (channel != null) { channel.Dispose(); } + + if (this.relyingParty != null) { + this.relyingParty.Dispose(); + } } } diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs index 445978e..e792a81 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs @@ -98,11 +98,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </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; } } @@ -118,11 +122,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </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; } } @@ -137,8 +145,17 @@ namespace DotNetOpenAuth.OpenId.Provider { /// before responding to the relying party's request. /// </remarks> public static IHostProcessedRequest PendingRequest { - get { return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest; } - set { HttpContext.Current.Session[PendingRequestKey] = value; } + 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> diff --git a/src/DotNetOpenAuth/OpenId/ProviderEndpointDescription.cs b/src/DotNetOpenAuth/OpenId/ProviderEndpointDescription.cs index 4cfbac5..6514ffd 100644 --- a/src/DotNetOpenAuth/OpenId/ProviderEndpointDescription.cs +++ b/src/DotNetOpenAuth/OpenId/ProviderEndpointDescription.cs @@ -8,6 +8,7 @@ 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; @@ -21,7 +22,7 @@ namespace DotNetOpenAuth.OpenId { /// This is an immutable type. /// </remarks> [Serializable] - internal class ProviderEndpointDescription : IProviderEndpoint { + internal sealed class ProviderEndpointDescription : IProviderEndpoint { /// <summary> /// Initializes a new instance of the <see cref="ProviderEndpointDescription"/> class. /// </summary> @@ -31,8 +32,9 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentNullException>(providerEndpoint != null); Contract.Requires<ArgumentNullException>(openIdVersion != null); - this.Endpoint = providerEndpoint; - this.ProtocolVersion = openIdVersion; + this.Uri = providerEndpoint; + this.Version = openIdVersion; + this.Capabilities = new ReadOnlyCollection<string>(EmptyList<string>.Instance); } /// <summary> @@ -44,42 +46,24 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentNullException>(providerEndpoint != null); Contract.Requires<ArgumentNullException>(serviceTypeURIs != null); - this.Endpoint = providerEndpoint; + 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.ProtocolVersion = opIdentifierProtocol.Version; + this.Version = opIdentifierProtocol.Version; } else if (claimedIdentifierProviderVersion != null) { - this.ProtocolVersion = claimedIdentifierProviderVersion.Version; + this.Version = claimedIdentifierProviderVersion.Version; + } else { + ErrorUtilities.ThrowProtocol(OpenIdStrings.ProviderVersionUnrecognized, this.Uri); } - - ErrorUtilities.VerifyProtocol(this.ProtocolVersion != null, OpenIdStrings.ProviderVersionUnrecognized, this.Endpoint); } - #region IProviderEndpoint Properties - - /// <summary> - /// Gets the detected version of OpenID implemented by the Provider. - /// </summary> - Version IProviderEndpoint.Version { - get { return this.ProtocolVersion; } - } - - /// <summary> - /// Gets the URL that the OpenID Provider receives authentication requests at. - /// </summary> - Uri IProviderEndpoint.Uri { - get { return this.Endpoint; } - } - - #endregion - /// <summary> /// Gets the URL that the OpenID Provider listens for incoming OpenID messages on. /// </summary> - internal Uri Endpoint { get; private set; } + public Uri Uri { get; private set; } /// <summary> /// Gets the OpenID protocol version this endpoint supports. @@ -88,14 +72,14 @@ namespace DotNetOpenAuth.OpenId { /// If an endpoint supports multiple versions, each version must be represented /// by its own <see cref="ProviderEndpointDescription"/> object. /// </remarks> - internal Version ProtocolVersion { get; private set; } + 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 Methods + #region IProviderEndpoint Members /// <summary> /// Checks whether the OpenId Identifier claims support for a given extension. @@ -111,10 +95,8 @@ namespace DotNetOpenAuth.OpenId { /// 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<T>() where T : IOpenIdMessageExtension, new() { - ErrorUtilities.VerifyOperation(this.Capabilities != null, OpenIdStrings.ExtensionLookupSupportUnavailable); - T extension = new T(); - return this.IsExtensionSupported(extension); + bool IProviderEndpoint.IsExtensionSupported<T>() { + throw new NotImplementedException(); } /// <summary> @@ -131,52 +113,22 @@ namespace DotNetOpenAuth.OpenId { /// 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) { - ErrorUtilities.VerifyOperation(this.Capabilities != null, OpenIdStrings.ExtensionLookupSupportUnavailable); - var extension = (IOpenIdMessageExtension)Activator.CreateInstance(extensionType); - return this.IsExtensionSupported(extension); + bool IProviderEndpoint.IsExtensionSupported(Type extensionType) { + throw new NotImplementedException(); } #endregion +#if CONTRACTS_FULL /// <summary> - /// Determines whether some extension is supported by the Provider. + /// Verifies conditions that should be true for any valid state of this object. /// </summary> - /// <param name="extensionUri">The extension URI.</param> - /// <returns> - /// <c>true</c> if the extension is supported; otherwise, <c>false</c>. - /// </returns> - protected internal bool IsExtensionSupported(string extensionUri) { - Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(extensionUri)); - Contract.Requires<InvalidOperationException>(this.Capabilities != null, OpenIdStrings.ExtensionLookupSupportUnavailable); - return this.Capabilities.Contains(extensionUri); - } - - /// <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> - protected internal bool IsExtensionSupported(IOpenIdMessageExtension extension) { - Contract.Requires<ArgumentNullException>(extension != null); - Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(extension.TypeUri)); - Contract.Requires<InvalidOperationException>(this.Capabilities != null, OpenIdStrings.ExtensionLookupSupportUnavailable); - - // Consider the primary case. - if (this.IsExtensionSupported(extension.TypeUri)) { - return true; - } - - // Consider the secondary cases. - if (extension.AdditionalSupportedTypeUris != null) { - if (extension.AdditionalSupportedTypeUris.Any(typeUri => this.IsExtensionSupported(typeUri))) { - return true; - } - } - - return false; + [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/Realm.cs b/src/DotNetOpenAuth/OpenId/Realm.cs index 600e6c0..98e3598 100644 --- a/src/DotNetOpenAuth/OpenId/Realm.cs +++ b/src/DotNetOpenAuth/OpenId/Realm.cs @@ -13,6 +13,7 @@ namespace DotNetOpenAuth.OpenId { using System.Globalization; using System.Linq; using System.Text.RegularExpressions; + using System.Web; using System.Xml; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Xrds; @@ -98,6 +99,40 @@ namespace DotNetOpenAuth.OpenId { : 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> @@ -145,6 +180,14 @@ namespace DotNetOpenAuth.OpenId { } /// <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 { @@ -219,6 +262,7 @@ namespace DotNetOpenAuth.OpenId { [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; } @@ -229,6 +273,7 @@ namespace DotNetOpenAuth.OpenId { /// <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; } @@ -239,6 +284,7 @@ namespace DotNetOpenAuth.OpenId { /// <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; } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs index bc2b6ca..ac70387 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs @@ -96,7 +96,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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(ProviderEndpointDescription provider) { + 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. @@ -104,7 +104,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return null; } - Association association = this.associationStore.GetAssociation(provider.Endpoint, this.SecuritySettings); + 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)) { @@ -124,7 +124,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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(ProviderEndpointDescription provider) { + internal Association GetOrCreateAssociation(IProviderEndpoint provider) { return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider); } @@ -141,7 +141,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// association store. /// Any new association is automatically added to the <see cref="associationStore"/>. /// </remarks> - private Association CreateNewAssociation(ProviderEndpointDescription provider) { + private Association CreateNewAssociation(IProviderEndpoint provider) { Contract.Requires<ArgumentNullException>(provider != null); // If there is no association store, there is no point in creating an association. @@ -160,7 +160,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // 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.Endpoint, ex); + Logger.OpenId.ErrorFormat("VerificationException occurred while trying to create an association with {0}. {1}", provider.Uri, ex); return null; } } @@ -175,7 +175,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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(ProviderEndpointDescription provider, AssociateRequest associateRequest, int retriesRemaining) { + private Association CreateNewAssociation(IProviderEndpoint provider, AssociateRequest associateRequest, int retriesRemaining) { Contract.Requires<ArgumentNullException>(provider != null); if (associateRequest == null || retriesRemaining < 0) { @@ -190,7 +190,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse; if (associateSuccessfulResponse != null) { Association association = associateSuccessfulResponse.CreateAssociation(associateRequest, null); - this.associationStore.StoreAssociation(provider.Endpoint, association); + this.associationStore.StoreAssociation(provider.Uri, association); return association; } else if (associateUnsuccessfulResponse != null) { if (string.IsNullOrEmpty(associateUnsuccessfulResponse.AssociationType)) { @@ -198,7 +198,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return null; } - if (!this.securitySettings.IsAssociationInPermittedRange(Protocol.Lookup(provider.ProtocolVersion), associateUnsuccessfulResponse.AssociationType)) { + 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; } @@ -209,7 +209,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } // Make sure the Provider isn't suggesting an incompatible pair of association/session types. - Protocol protocol = Protocol.Lookup(provider.ProtocolVersion); + Protocol protocol = Protocol.Lookup(provider.Version); ErrorUtilities.VerifyProtocol( HmacShaAssociation.IsDHSessionCompatible(protocol, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType), OpenIdStrings.IncompatibleAssociationAndSessionTypes, @@ -231,7 +231,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // 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.Endpoint, ex); + Logger.OpenId.ErrorFormat("An error occurred while trying to create an association with {0}. {1}", provider.Uri, ex); return null; } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs index def8f34..09383bb 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs @@ -4,8 +4,6 @@ // </copyright> //----------------------------------------------------------------------- -using System.Threading; - namespace DotNetOpenAuth.OpenId.RelyingParty { using System; using System.Collections.Generic; @@ -13,6 +11,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Diagnostics.Contracts; using System.Linq; using System.Text; + using System.Threading; using System.Web; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.ChannelElements; @@ -34,12 +33,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private readonly OpenIdRelyingParty RelyingParty; /// <summary> - /// The endpoint that describes the particular OpenID Identifier and Provider that - /// will be used to create the authentication request. - /// </summary> - private readonly ServiceEndpoint endpoint; - - /// <summary> /// How an association may or should be created or used in the formulation of the /// authentication request. /// </summary> @@ -69,17 +62,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class. /// </summary> - /// <param name="endpoint">The endpoint that describes the OpenID Identifier and Provider that will complete the authentication.</param> + /// <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(ServiceEndpoint endpoint, Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) { - Contract.Requires<ArgumentNullException>(endpoint != null); + 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.endpoint = endpoint; + this.DiscoveryResult = discoveryResult; this.RelyingParty = relyingParty; this.Realm = realm; this.ReturnToUrl = returnToUrl; @@ -139,7 +132,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// property for a null value. /// </remarks> public Identifier ClaimedIdentifier { - get { return this.IsDirectedIdentity ? null : this.endpoint.ClaimedIdentifier; } + get { return this.IsDirectedIdentity ? null : this.DiscoveryResult.ClaimedIdentifier; } } /// <summary> @@ -147,7 +140,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// determine and send the ClaimedIdentifier after authentication. /// </summary> public bool IsDirectedIdentity { - get { return this.endpoint.ClaimedIdentifier == this.endpoint.Protocol.ClaimedIdentifierForOPIdentifier; } + get { return this.DiscoveryResult.ClaimedIdentifier == this.DiscoveryResult.Protocol.ClaimedIdentifierForOPIdentifier; } } /// <summary> @@ -166,9 +159,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// location. /// </summary> public IProviderEndpoint Provider { - get { return this.endpoint; } + 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> @@ -194,13 +193,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { get { return this.extensions; } } - /// <summary> - /// Gets the service endpoint. - /// </summary> - internal ServiceEndpoint Endpoint { - get { return this.endpoint; } - } - #region IAuthenticationRequest methods /// <summary> @@ -367,12 +359,23 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { ErrorUtilities.VerifyProtocol(realm.Contains(returnToUrl), OpenIdStrings.ReturnToNotUnderRealm, returnToUrl, realm); // Perform discovery right now (not deferred). - IEnumerable<ServiceEndpoint> serviceEndpoints; + IEnumerable<IdentifierDiscoveryResult> serviceEndpoints; try { - serviceEndpoints = userSuppliedIdentifier.Discover(relyingParty.WebRequestHandler); + 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 = EmptyList<ServiceEndpoint>.Instance; + serviceEndpoints = Enumerable.Empty<IdentifierDiscoveryResult>(); } // Filter disallowed endpoints. @@ -383,6 +386,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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> /// Performs deferred request generation for the <see cref="Create"/> method. /// </summary> /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> @@ -399,7 +414,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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<ServiceEndpoint> serviceEndpoints, bool createNewAssociationsAsNeeded) { + 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"); @@ -411,12 +426,12 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { ErrorUtilities.VerifyOperation(!relyingParty.SecuritySettings.RequireAssociation || relyingParty.AssociationManager.HasAssociationStore, OpenIdStrings.AssociationStoreRequired); Logger.Yadis.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier); - IEnumerable<ServiceEndpoint> endpoints = FilterAndSortEndpoints(serviceEndpoints, relyingParty); + 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<ServiceEndpoint>(0); + var failedAssociationEndpoints = new List<IdentifierDiscoveryResult>(0); foreach (var endpoint in endpoints) { Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier); @@ -427,7 +442,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // 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.ProviderDescription) : relyingParty.AssociationManager.GetExistingAssociation(endpoint.ProviderDescription); + 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); @@ -470,17 +485,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <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<ServiceEndpoint> FilterAndSortEndpoints(IEnumerable<ServiceEndpoint> endpoints, OpenIdRelyingParty relyingParty) { + private static List<IdentifierDiscoveryResult> FilterAndSortEndpoints(IEnumerable<IdentifierDiscoveryResult> endpoints, OpenIdRelyingParty relyingParty) { Contract.Requires<ArgumentNullException>(endpoints != null); Contract.Requires<ArgumentNullException>(relyingParty != null); // Construct the endpoints filters based on criteria given by the host web site. - EndpointSelector versionFilter = ep => ((ServiceEndpoint)ep).Protocol.Version >= Protocol.Lookup(relyingParty.SecuritySettings.MinimumRequiredOpenIdVersion).Version; + EndpointSelector versionFilter = ep => ep.Version >= Protocol.Lookup(relyingParty.SecuritySettings.MinimumRequiredOpenIdVersion).Version; EndpointSelector hostingSiteFilter = relyingParty.EndpointFilter ?? (ep => true); bool anyFilteredOut = false; - var filteredEndpoints = new List<IXrdsProviderEndpoint>(); - foreach (ServiceEndpoint endpoint in endpoints) { + var filteredEndpoints = new List<IdentifierDiscoveryResult>(); + foreach (var endpoint in endpoints) { if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) { filteredEndpoints.Add(endpoint); } else { @@ -489,10 +504,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } // Sort endpoints so that the first one in the list is the most preferred one. - filteredEndpoints.Sort(relyingParty.EndpointOrder); + filteredEndpoints.OrderBy(ep => ep, relyingParty.EndpointOrder); - List<ServiceEndpoint> endpointList = new List<ServiceEndpoint>(filteredEndpoints.Count); - foreach (ServiceEndpoint endpoint in filteredEndpoints) { + var endpointList = new List<IdentifierDiscoveryResult>(filteredEndpoints.Count); + foreach (var endpoint in filteredEndpoints) { endpointList.Add(endpoint); } @@ -521,20 +536,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { SignedResponseRequest request; if (!this.IsExtensionOnly) { - CheckIdRequest authRequest = new CheckIdRequest(this.endpoint.Protocol.Version, this.endpoint.ProviderEndpoint, this.Mode); - authRequest.ClaimedIdentifier = this.endpoint.ClaimedIdentifier; - authRequest.LocalIdentifier = this.endpoint.ProviderLocalIdentifier; + 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.endpoint.Protocol.Version, this.endpoint.ProviderEndpoint, this.Mode); + 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.endpoint.UserSuppliedIdentifier != null) { - request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.endpoint.UserSuppliedIdentifier.OriginalString); + if (this.DiscoveryResult.UserSuppliedIdentifier != null) { + request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.DiscoveryResult.UserSuppliedIdentifier.OriginalString); } foreach (IOpenIdMessageExtension extension in this.extensions) { request.Extensions.Add(extension); @@ -551,7 +566,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Association association = null; switch (this.associationPreference) { case AssociationPreference.IfPossible: - association = this.RelyingParty.AssociationManager.GetOrCreateAssociation(this.endpoint.ProviderDescription); + association = this.RelyingParty.AssociationManager.GetOrCreateAssociation(this.DiscoveryResult); if (association == null) { // Avoid trying to create the association again if the redirecting response // is generated again. @@ -559,7 +574,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } break; case AssociationPreference.IfAlreadyEstablished: - association = this.RelyingParty.AssociationManager.GetExistingAssociation(this.endpoint.ProviderDescription); + association = this.RelyingParty.AssociationManager.GetExistingAssociation(this.DiscoveryResult); break; case AssociationPreference.Never: break; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs new file mode 100644 index 0000000..94eb5ba --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// <copyright file="DuplicateRequestedHostsComparer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// An authentication request comparer that judges equality solely on the OP endpoint hostname. + /// </summary> + internal class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { + /// <summary> + /// The singleton instance of this comparer. + /// </summary> + private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); + + /// <summary> + /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. + /// </summary> + private DuplicateRequestedHostsComparer() { + } + + /// <summary> + /// Gets the singleton instance of this comparer. + /// </summary> + internal static IEqualityComparer<IAuthenticationRequest> Instance { + get { return instance; } + } + + #region IEqualityComparer<IAuthenticationRequest> Members + + /// <summary> + /// Determines whether the specified objects are equal. + /// </summary> + /// <param name="x">The first object to compare.</param> + /// <param name="y">The second object to compare.</param> + /// <returns> + /// true if the specified objects are equal; otherwise, false. + /// </returns> + public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { + if (x == null && y == null) { + return true; + } + + if (x == null || y == null) { + return false; + } + + // We'll distinguish based on the host name only, which + // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, + // this multiple OP attempt thing was just a convenience feature anyway. + return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Returns a hash code for the specified object. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> + /// <returns>A hash code for the specified object.</returns> + /// <exception cref="T:System.ArgumentNullException"> + /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. + /// </exception> + public int GetHashCode(IAuthenticationRequest obj) { + return obj.Provider.Uri.Host.GetHashCode(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs index 3808c85..65db0bd 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequest.cs @@ -97,6 +97,12 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequestContract.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequestContract.cs index 41cc4e9..cd36cc7 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequestContract.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IAuthenticationRequestContract.cs @@ -32,11 +32,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } Realm IAuthenticationRequest.Realm { - get { throw new NotImplementedException(); } + get { + Contract.Ensures(Contract.Result<Realm>() != null); + throw new NotImplementedException(); + } } Identifier IAuthenticationRequest.ClaimedIdentifier { - get { throw new NotImplementedException(); } + get { + throw new NotImplementedException(); + } } bool IAuthenticationRequest.IsDirectedIdentity { @@ -54,7 +59,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } IProviderEndpoint IAuthenticationRequest.Provider { - get { throw new NotImplementedException(); } + 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) { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs index a90ddd4..5d8918d 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/IProviderEndpoint.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System; + using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; @@ -30,6 +31,9 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <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> @@ -45,6 +49,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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> @@ -59,6 +64,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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); } @@ -67,20 +73,32 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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 { throw new NotImplementedException(); } + 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 { throw new NotImplementedException(); } + get { + Contract.Ensures(Contract.Result<Uri>() != null); + throw new System.NotImplementedException(); + } } /// <summary> @@ -118,7 +136,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { bool IProviderEndpoint.IsExtensionSupported(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(); } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs deleted file mode 100644 index 89b4ef0..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpoint.cs +++ /dev/null @@ -1,41 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="IXrdsProviderEndpoint.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.OpenId.RelyingParty { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Diagnostics.Contracts; - - /// <summary> - /// An <see cref="IProviderEndpoint"/> interface with additional members for use - /// in sorting for most preferred endpoint. - /// </summary> - [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds", Justification = "Xrds is an acronym.")] - [ContractClass(typeof(IXrdsProviderEndpointContract))] - public interface IXrdsProviderEndpoint : IProviderEndpoint { - /// <summary> - /// Gets the priority associated with this service that may have been given - /// in the XRDS document. - /// </summary> - int? ServicePriority { get; } - - /// <summary> - /// Gets the priority associated with the service endpoint URL. - /// </summary> - /// <remarks> - /// When sorting by priority, this property should be considered second after - /// <see cref="ServicePriority"/>. - /// </remarks> - int? UriPriority { get; } - - /// <summary> - /// Checks for the presence of a given Type URI in an XRDS service. - /// </summary> - /// <param name="typeUri">The type URI to check for.</param> - /// <returns><c>true</c> if the service type uri is present; <c>false</c> otherwise.</returns> - bool IsTypeUriPresent(string typeUri); - } -} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpointContract.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpointContract.cs deleted file mode 100644 index e0e2b0b..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/IXrdsProviderEndpointContract.cs +++ /dev/null @@ -1,59 +0,0 @@ -// <auto-generated /> - -namespace DotNetOpenAuth.OpenId.RelyingParty { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Diagnostics.Contracts; - using System.Globalization; - using DotNetOpenAuth.OpenId.Messages; - - [ContractClassFor(typeof(IXrdsProviderEndpoint))] - internal abstract class IXrdsProviderEndpointContract : IXrdsProviderEndpoint { - #region IXrdsProviderEndpoint Properties - - int? IXrdsProviderEndpoint.ServicePriority { - get { throw new System.NotImplementedException(); } - } - - int? IXrdsProviderEndpoint.UriPriority { - get { throw new System.NotImplementedException(); } - } - - #endregion - - #region IProviderEndpoint Properties - - Version IProviderEndpoint.Version { - get { throw new System.NotImplementedException(); } - } - - Uri IProviderEndpoint.Uri { - get { throw new System.NotImplementedException(); } - } - - #endregion - - #region IXrdsProviderEndpoint Methods - - bool IXrdsProviderEndpoint.IsTypeUriPresent(string typeUri) { - throw new System.NotImplementedException(); - } - - #endregion - - #region IProviderEndpoint Methods - - bool IProviderEndpoint.IsExtensionSupported<T>() { - throw new System.NotImplementedException(); - } - - bool IProviderEndpoint.IsExtensionSupported(System.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 System.NotImplementedException(); - } - - #endregion - } -} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs new file mode 100644 index 0000000..ae9fbdc --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs @@ -0,0 +1,238 @@ +//----------------------------------------------------------------------- +// <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.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(IRelyingPartyApplicationStore 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 (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { + req.SetUntrustedCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, userSuppliedIdentifier.OriginalString); + } + + // Our javascript needs to let the user know which endpoint responded. So we force it here. + // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); + + // Inform ourselves in return_to that we're in a popup or iframe. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.UIPopupCallbackKey, "1"); + + // We append a # at the end so that if the OP happens to support it, + // the OpenID response "query string" is appended after the hash rather than before, resulting in the + // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version + // of the static resource down from the server merely because of a changed URL. + // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html + ////TODO: + + yield return req; + } + } + + /// <summary> + /// Serializes discovery results on some <i>single</i> identifier on behalf of Javascript running on the browser. + /// </summary> + /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param> + /// <returns> + /// The JSON result to return to the user agent. + /// </returns> + /// <remarks> + /// We prepare a JSON object with this interface: + /// <code> + /// class jsonResponse { + /// string claimedIdentifier; + /// Array requests; // never null + /// string error; // null if no error + /// } + /// </code> + /// Each element in the requests array looks like this: + /// <code> + /// class jsonAuthRequest { + /// string endpoint; // URL to the OP endpoint + /// string immediate; // URL to initiate an immediate request + /// string setup; // URL to initiate a setup request. + /// } + /// </code> + /// </remarks> + public OutgoingWebResponse AsAjaxDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + var serializer = new JavaScriptSerializer(); + return new OutgoingWebResponse { + Body = serializer.Serialize(this.AsJsonDiscoveryResult(requests)), + }; + } + + /// <summary> + /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries + /// an AJAX-aware OpenID control. + /// </summary> + /// <param name="requests">The discovery results to serialize as a JSON response.</param> + /// <returns> + /// The JSON result to return to the user agent. + /// </returns> + public string AsAjaxPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + var serializer = new JavaScriptSerializer(); + string json = serializer.Serialize(this.AsJsonPreloadedDiscoveryResult(requests)); + + string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + json + ");"; + return script; + } + + /// <summary> + /// Converts a sequence of authentication requests to a JSON object for seeding an AJAX-enabled login page. + /// </summary> + /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param> + /// <returns>A JSON object, not yet serialized.</returns> + internal object AsJsonDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + requests = requests.CacheGeneratedResults(); + + if (requests.Any()) { + return new { + claimedIdentifier = (string)requests.First().ClaimedIdentifier, + requests = requests.Select(req => new { + endpoint = req.Provider.Uri.AbsoluteUri, + immediate = this.GetRedirectUrl(req, true), + setup = this.GetRedirectUrl(req, false), + }).ToArray() + }; + } else { + return new { + requests = new object[0], + error = OpenIdStrings.OpenIdEndpointNotFound, + }; + } + } + + /// <summary> + /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries + /// an AJAX-aware OpenID control. + /// </summary> + /// <param name="requests">The discovery results to serialize as a JSON response.</param> + /// <returns> + /// A JSON object, not yet serialized to a string. + /// </returns> + private object AsJsonPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + // We prepare a JSON object with this interface: + // Array discoveryWrappers; + // Where each element in the above array has this interface: + // class discoveryWrapper { + // string userSuppliedIdentifier; + // jsonResponse discoveryResult; // contains result of call to SerializeDiscoveryAsJson(Identifier) + // } + var json = (from request in requests + group request by request.DiscoveryResult.UserSuppliedIdentifier into requestsByIdentifier + select new { + userSuppliedIdentifier = (string)requestsByIdentifier.Key, + discoveryResult = this.AsJsonDiscoveryResult(requestsByIdentifier), + }).ToArray(); + + return json; + } + + /// <summary> + /// Gets the full URL that carries an OpenID message, even if it exceeds the normal maximum size of a URL, + /// for purposes of sending to an AJAX component running in the browser. + /// </summary> + /// <param name="request">The authentication request.</param> + /// <param name="immediate"><c>true</c>to create a checkid_immediate request; + /// <c>false</c> to create a checkid_setup request.</param> + /// <returns>The absolute URL that carries the entire OpenID message.</returns> + private Uri GetRedirectUrl(IAuthenticationRequest request, bool immediate) { + Contract.Requires<ArgumentNullException>(request != null); + + request.Mode = immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; + return request.RedirectingResponse.GetDirectUriRequest(this.Channel); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs index 097d065..d80bf6a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -64,6 +64,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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> @@ -221,11 +226,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string AuthenticationFailedToolTipDefault = "Authentication failed."; /// <summary> - /// The default value for the <see cref="Throttle"/> property. - /// </summary> - private const int ThrottleDefault = 3; - - /// <summary> /// The default value for the <see cref="LogOnText"/> property. /// </summary> private const string LogOnTextDefault = "LOG IN"; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs index f89ec0a..4aa78a5 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs @@ -643,11 +643,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { set { base.UsePersistentCookie = value; - // use conditional here to prevent infinite recursion - // with CheckedChanged event. - bool rememberMe = value != LogOnPersistence.Session; - if (this.rememberMeCheckBox.Checked != rememberMe) { - this.rememberMeCheckBox.Checked = rememberMe; + 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; + } } } } @@ -700,79 +702,104 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // top row, left cell cell = new TableCell(); - this.label = new HtmlGenericControl("label"); - this.label.InnerText = LabelTextDefault; - cell.Controls.Add(this.label); - row1.Cells.Add(cell); + 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(); - cell.Controls.Add(new InPlaceControl(this)); - row1.Cells.Add(cell); + try { + cell.Controls.Add(new InPlaceControl(this)); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // top row, right cell cell = new TableCell(); - this.loginButton = new Button(); - this.loginButton.ID = "loginButton"; - this.loginButton.Text = ButtonTextDefault; - this.loginButton.ToolTip = ButtonToolTipDefault; - this.loginButton.Click += this.LoginButton_Click; - this.loginButton.ValidationGroup = ValidationGroupDefault; + try { + this.loginButton = new Button(); + this.loginButton.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; + this.panel.DefaultButton = this.loginButton.ID; #endif - cell.Controls.Add(this.loginButton); - row1.Cells.Add(cell); + 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(); - 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); + 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(); - 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); + 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(); @@ -780,17 +807,27 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // bottom row, middle cell cell = new TableCell(); - this.rememberMeCheckBox = new CheckBox(); - this.rememberMeCheckBox.Text = RememberMeTextDefault; - this.rememberMeCheckBox.Checked = RememberMeDefault; - this.rememberMeCheckBox.Visible = RememberMeVisibleDefault; - this.rememberMeCheckBox.CheckedChanged += this.RememberMeCheckBox_CheckedChanged; - cell.Controls.Add(this.rememberMeCheckBox); - row3.Cells.Add(cell); + 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(); - row3.Cells.Add(cell); + try { + row3.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // this sets all the controls' tab indexes this.TabIndex = TabIndexDefault; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs index dbf9530..8684bd1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs @@ -762,13 +762,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { IRelyingPartyApplicationStore store = this.Stateless ? null : (this.CustomApplicationStore ?? DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore)); var rp = new OpenIdRelyingParty(store); - - // Only set RequireSsl to true, as we don't want to override - // a .config setting of true with false. - if (this.RequireSsl) { - rp.SecuritySettings.RequireSsl = true; + 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; } - return rp; } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index 5e67d5b..a416f3a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -12,7 +12,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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; @@ -30,13 +34,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <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(IXrdsProviderEndpoint endpoint); + public delegate bool EndpointSelector(IProviderEndpoint endpoint); /// <summary> - /// Provides the programmatic facilities to act as an OpenId consumer. + /// Provides the programmatic facilities to act as an OpenID relying party. /// </summary> [ContractVerification(true)] - public sealed class OpenIdRelyingParty : IDisposable { + 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. @@ -49,6 +53,27 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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; @@ -56,7 +81,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Backing store for the <see cref="EndpointOrder"/> property. /// </summary> - private Comparison<IXrdsProviderEndpoint> endpointOrder = DefaultEndpointOrder; + private Comparison<IdentifierDiscoveryResult> endpointOrder = DefaultEndpointOrder; /// <summary> /// Backing field for the <see cref="Channel"/> property. @@ -73,7 +98,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. /// </summary> - /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param> + /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param> public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore) : this(applicationStore, applicationStore) { } @@ -90,6 +115,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Contract.Requires<ArgumentException>(associationStore == 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); @@ -98,11 +128,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // 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) { - if (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 (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; } this.channel = new OpenIdChannel(associationStore, nonceStore, this.SecuritySettings); @@ -119,8 +149,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Endpoints lacking any priority value are sorted to the end of the list. /// </remarks> [EditorBrowsable(EditorBrowsableState.Advanced)] - public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder { - get { return ServiceEndpoint.EndpointOrder; } + public static Comparison<IdentifierDiscoveryResult> DefaultEndpointOrder { + get { return IdentifierDiscoveryResult.EndpointOrder; } } /// <summary> @@ -202,7 +232,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// can be set to the value of <see cref="DefaultEndpointOrder"/>. /// </remarks> [EditorBrowsable(EditorBrowsableState.Advanced)] - public Comparison<IXrdsProviderEndpoint> EndpointOrder { + public Comparison<IdentifierDiscoveryResult> EndpointOrder { get { return this.endpointOrder; } @@ -232,6 +262,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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> @@ -253,6 +290,24 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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> @@ -372,13 +427,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <para>No exception is thrown if no OpenID endpoints were discovered. /// An empty enumerable is returned instead.</para> /// </remarks> - public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + 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>(); + return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>().CacheGeneratedResults(); } /// <summary> @@ -459,21 +514,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null); - // Build the realm URL - UriBuilder realmUrl = new UriBuilder(this.Channel.GetRequestFromContext().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 this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri)); + return this.CreateRequests(userSuppliedIdentifier, Realm.AutoDetect); } /// <summary> @@ -484,6 +525,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <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()); } @@ -530,6 +572,52 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } + /// <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> @@ -577,11 +665,112 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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> + 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, "OpenID popup window or iframe did not recognize an OpenID response in the request."); + + // 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> /// 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) { + 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) { @@ -591,6 +780,42 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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) {{ + objSrc.{0}; + 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> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index 0254346..f22645f 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -16,6 +16,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Linq; using System.Text; using System.Web; + using System.Web.Script.Serialization; using System.Web.UI; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -31,30 +32,30 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal const string EmbeddedAjaxJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.js"; /// <summary> - /// The name of the javascript function that will initiate a synchronous callback. + /// The "dnoa.op_endpoint" string. /// </summary> - protected const string CallbackJSFunction = "window.dnoa_internal.callback"; + internal const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; /// <summary> - /// The name of the javascript function that will initiate an asynchronous callback. + /// The "dnoa.claimed_id" string. /// </summary> - protected const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; + 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> - private const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; + internal const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; /// <summary> - /// The "dnoa.op_endpoint" string. + /// The name of the javascript function that will initiate an asynchronous callback. /// </summary> - private const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; + protected internal const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; /// <summary> - /// The "dnoa.claimed_id" string. + /// The name of the javascript function that will initiate a synchronous callback. /// </summary> - private const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; + protected const string CallbackJSFunction = "window.dnoa_internal.callback"; #region Property viewstate keys @@ -86,11 +87,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.None; /// <summary> - /// Backing field for the <see cref="RelyingPartyNonVerifying"/> property. - /// </summary> - private static OpenIdRelyingParty relyingPartyNonVerifying; - - /// <summary> /// The authentication response that just came in. /// </summary> private IAuthenticationResponse authenticationResponse; @@ -102,12 +98,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private string discoveryResult; /// <summary> - /// A dictionary of extension response types and the javascript member - /// name to map them to on the user agent. - /// </summary> - private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingPartyAjaxControlBase"/> class. /// </summary> protected OpenIdRelyingPartyAjaxControlBase() { @@ -152,6 +142,31 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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 { @@ -193,22 +208,17 @@ namespace DotNetOpenAuth.OpenId.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). + /// Gets the relying party as its AJAX type. /// </summary> - /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value> - protected abstract string OpenIdAuthDataFormKey { get; } + protected OpenIdAjaxRelyingParty AjaxRelyingParty { + get { return (OpenIdAjaxRelyingParty)this.RelyingParty; } + } /// <summary> - /// Gets the relying party to use when verification of incoming messages is NOT wanted. + /// 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> - private static OpenIdRelyingParty RelyingPartyNonVerifying { - get { - if (relyingPartyNonVerifying == null) { - relyingPartyNonVerifying = OpenIdRelyingParty.CreateNonVerifying(); - } - return relyingPartyNonVerifying; - } - } + /// <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 @@ -232,11 +242,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [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); + this.RelyingParty.RegisterClientScriptExtension<T>(propertyName); } #region ICallbackEventHandler Members @@ -263,27 +269,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #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) { - // 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 { - // 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> /// Returns the results of a callback event that targets a control. /// </summary> /// <returns>The result of the callback.</returns> @@ -305,7 +290,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); this.Identifier = userSuppliedIdentifier; - this.discoveryResult = this.SerializeDiscoveryAsJson(this.Identifier); + + 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(IRelyingPartyApplicationStore store) { + return new OpenIdAjaxRelyingParty(store); } /// <summary> @@ -323,8 +320,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="identifiers">The identifiers to perform discovery on.</param> protected void PreloadDiscovery(IEnumerable<Identifier> identifiers) { - string discoveryResults = this.SerializeDiscoveryAsJson(identifiers); - string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + discoveryResults + ");"; + string script = this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResult( + identifiers.SelectMany(id => this.CreateRequests(id))); this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), this.ClientID, script, true); } @@ -402,6 +399,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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 @@ -420,172 +418,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Notifies the user agent via an AJAX response of a completed authentication attempt. /// </summary> protected override void ScriptClosingPopupOrIFrame() { - Logger.OpenId.DebugFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); - string extensionsJson = null; - - var authResponse = RelyingPartyNonVerifying.GetResponse(); - Logger.Controls.DebugFormat( - "The {0} control checked for an authentication response from a popup window or iframe using a non-verifying RP and found: {1}", - this.ID, - authResponse.Status); - if (authResponse.Status == AuthenticationStatus.Authenticated) { - this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. - 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; - } + Action<AuthenticationStatus> callback = status => { + if (status == AuthenticationStatus.Authenticated) { + this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. } + }; - extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true); - } - - string payload = "document.URL"; - if (Page.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(Page.Request.Url); - payloadUri.AppendQueryArgs(Page.Request.Form.ToDictionary()); - payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); - } - - if (!string.IsNullOrEmpty(extensionsJson)) { - payload += ", " + extensionsJson; - } - - this.CallbackUserAgentMethod("dnoa_internal.processAuthorizationResult(" + payload + ")"); - } - - /// <summary> - /// Serializes the discovery of multiple identifiers as a JSON object. - /// </summary> - /// <param name="identifiers">The identifiers to perform discovery on and create requests for.</param> - /// <returns>The serialized JSON object.</returns> - private string SerializeDiscoveryAsJson(IEnumerable<Identifier> identifiers) { - ErrorUtilities.VerifyArgumentNotNull(identifiers, "identifiers"); - - // 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) - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("["); - foreach (var identifier in identifiers) { // TODO: parallelize discovery on these identifiers - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("userSuppliedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(identifier)); - discoveryResultBuilder.AppendFormat("discoveryResult: {0}", this.SerializeDiscoveryAsJson(identifier)); - discoveryResultBuilder.Append("},"); - } - - discoveryResultBuilder.Length -= 1; // trim last comma - discoveryResultBuilder.Append("]"); - return discoveryResultBuilder.ToString(); - } - - /// <summary> - /// Serializes the results of discovery and the created auth requests as a JSON object - /// for the user agent to initiate. - /// </summary> - /// <param name="identifier">The identifier to perform discovery on.</param> - /// <returns>The JSON string.</returns> - private string SerializeDiscoveryAsJson(Identifier identifier) { - ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); - - // We prepare a JSON object with this interface: - // class jsonResponse { - // string claimedIdentifier; - // Array requests; // never null - // string error; // null if no error - // } - // Each element in the requests array looks like this: - // class jsonAuthRequest { - // string endpoint; // URL to the OP endpoint - // string immediate; // URL to initiate an immediate request - // string setup; // URL to initiate a setup request. - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("{"); - try { - IEnumerable<IAuthenticationRequest> requests = this.CreateRequests(identifier).CacheGeneratedResults(); - if (requests.Any()) { - discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); - discoveryResultBuilder.Append("requests: ["); - foreach (IAuthenticationRequest request in requests) { - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("endpoint: {0},", MessagingUtilities.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Immediate; - OutgoingWebResponse response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("immediate: {0},", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Setup; - response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("setup: {0}", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - discoveryResultBuilder.Append("},"); - } - discoveryResultBuilder.Length -= 1; // trim off last comma - discoveryResultBuilder.Append("]"); - } else { - discoveryResultBuilder.Append("requests: [],"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); - } - } catch (ProtocolException ex) { - discoveryResultBuilder.Append("requests: [],"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); - } - - discoveryResultBuilder.Append("}"); - return discoveryResultBuilder.ToString(); - } - - /// <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) { - ErrorUtilities.VerifyArgumentNotNull(requests, "requests"); // NO CODE CONTRACTS! (yield return used here) - - // Configure each generated request. - int reqIndex = 0; - foreach (var req in requests) { - req.SetUntrustedCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); - - // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter - if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { - Identifier userSuppliedIdentifier = ((AuthenticationRequest)req).Endpoint.UserSuppliedIdentifier; - 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(OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); - req.SetUntrustedCallbackArgument(ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); - - // Inform ourselves in return_to that we're in a popup or iframe. - req.SetUntrustedCallbackArgument(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: + OutgoingWebResponse response = this.RelyingParty.ProcessResponseFromPopup( + this.RelyingParty.Channel.GetRequestFromContext(), + callback); - yield return req; - } + response.Send(); } /// <summary> @@ -615,33 +458,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, - /// and closes the calling popup window if applicable. - /// </summary> - /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including - /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> - private void CallbackUserAgentMethod(string methodCall) { - Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); - Page.Response.Write(@"<html><body><script language='javascript'> - 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) {{ - objSrc.{0}; - window.self.close(); - }} else {{ - objSrc.{0}; - }} -</script></body></html>"; - Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall)); - Page.Response.End(); - } - - /// <summary> /// Sets the window.aspnetapppath variable on the user agent so that cookies can be set with the proper path. /// </summary> private void SetWebAppPathOnUserAgent() { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js index 6faad56..4de5188 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js @@ -701,7 +701,7 @@ window.dnoa_internal.PositiveAssertion = function(uri) { }; window.dnoa_internal.clone = function(obj) { - if (obj === null || typeof (obj) != 'object') { + if (obj === null || typeof (obj) != 'object' || !isNaN(obj)) { // !isNaN catches Date objects return obj; } @@ -710,6 +710,10 @@ window.dnoa_internal.clone = function(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; }; @@ -732,7 +736,7 @@ window.dnoa_internal.clearExpiredPositiveAssertions = function() { 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].result === window.dnoa_internal.authSuccess) { + 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. diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index 02df7a2..5090ecd 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -92,6 +92,21 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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> @@ -111,18 +126,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion - #region Callback parameter names - - /// <summary> - /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. - /// </summary> - protected 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 const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + #region Private callback parameter names /// <summary> /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. @@ -293,10 +297,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// heavily trafficked web pages. /// </remarks> [Browsable(false)] - public OpenIdRelyingParty RelyingParty { + public virtual OpenIdRelyingParty RelyingParty { get { if (this.relyingParty == null) { this.relyingParty = this.CreateRelyingParty(); + this.ConfigureRelyingParty(this.relyingParty); this.relyingPartyOwned = true; } return this.relyingParty; @@ -353,6 +358,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } 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. @@ -556,8 +563,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected internal virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) { Contract.Requires<ArgumentNullException>(identifier != null); - // Delegate to a private method to keep 'yield return' and Code Contract separate. - return this.CreateRequestsCore(identifier); + // 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> @@ -634,6 +648,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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() { @@ -769,29 +790,32 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Creates the relying party instance used to generate authentication requests. /// </summary> /// <returns>The instantiated relying party.</returns> - protected virtual OpenIdRelyingParty CreateRelyingParty() { - return this.CreateRelyingParty(true); + protected OpenIdRelyingParty CreateRelyingParty() { + IRelyingPartyApplicationStore 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="verifySignature"> - /// A value indicating whether message protections should be applied to the processed messages. - /// Use <c>false</c> to postpone verification to a later time without invalidating nonces. - /// </param> + /// <param name="store">The store to pass to the relying party constructor.</param> /// <returns>The instantiated relying party.</returns> - protected virtual OpenIdRelyingParty CreateRelyingParty(bool verifySignature) { - IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); - var rp = verifySignature ? new OpenIdRelyingParty(store) : OpenIdRelyingParty.CreateNonVerifying(); + protected virtual OpenIdRelyingParty CreateRelyingParty(IRelyingPartyApplicationStore store) { + return new OpenIdRelyingParty(store); + } + + /// <summary> + /// Configures the relying party. + /// </summary> + /// <param name="relyingParty">The relying party.</param> + 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) { - rp.SecuritySettings.RequireSsl = true; + relyingParty.SecuritySettings.RequireSsl = true; } - - return rp; } /// <summary> @@ -810,7 +834,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { case PopupBehavior.Always: return true; case PopupBehavior.IfProviderSupported: - return request.Provider.IsExtensionSupported<UIRequest>(); + return request.DiscoveryResult.IsExtensionSupported<UIRequest>(); default: throw ErrorUtilities.ThrowInternal("Unexpected value for Popup property."); } @@ -841,21 +865,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Wires the popup window to close itself and pass the authentication result to the parent window. - /// </summary> - protected virtual void ScriptClosingPopupOrIFrame() { - StringBuilder startupScript = new StringBuilder(); - startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL);"); - startupScript.AppendLine("window.close();"); - - this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true); - - // TODO: alternately we should probably take over rendering this page here to avoid - // a lot of unnecessary work on the server and possible momentary display of the - // page in the popup window. - } - - /// <summary> /// 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> @@ -937,9 +946,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // Inform ourselves in return_to that we're in a popup. req.SetUntrustedCallbackArgument(UIPopupCallbackKey, "1"); - if (req.Provider.IsExtensionSupported<UIRequest>()) { + 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()); + // 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. @@ -1029,67 +1042,5 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return false; } - - /// <summary> - /// An authentication request comparer that judges equality solely on the OP endpoint hostname. - /// </summary> - private class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { - /// <summary> - /// The singleton instance of this comparer. - /// </summary> - private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); - - /// <summary> - /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. - /// </summary> - private DuplicateRequestedHostsComparer() { - } - - /// <summary> - /// Gets the singleton instance of this comparer. - /// </summary> - internal static IEqualityComparer<IAuthenticationRequest> Instance { - get { return instance; } - } - - #region IEqualityComparer<IAuthenticationRequest> Members - - /// <summary> - /// Determines whether the specified objects are equal. - /// </summary> - /// <param name="x">The first object to compare.</param> - /// <param name="y">The second object to compare.</param> - /// <returns> - /// true if the specified objects are equal; otherwise, false. - /// </returns> - public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { - if (x == null && y == null) { - return true; - } - - if (x == null || y == null) { - return false; - } - - // We'll distinguish based on the host name only, which - // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, - // this multiple OP attempt thing was just a convenience feature anyway. - return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Returns a hash code for the specified object. - /// </summary> - /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> - /// <returns>A hash code for the specified object.</returns> - /// <exception cref="T:System.ArgumentNullException"> - /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. - /// </exception> - public int GetHashCode(IAuthenticationRequest obj) { - return obj.Provider.Uri.Host.GetHashCode(); - } - - #endregion - } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs index e93383d..b7a54eb 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs @@ -81,11 +81,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private HiddenField positiveAssertionField; /// <summary> - /// A field to store the value to set on the <see cref="textBox"/> control after it's created. - /// </summary> - private bool downloadYuiLibrary = OpenIdAjaxTextBox.DownloadYahooUILibraryDefault; - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdSelector"/> class. /// </summary> public OpenIdSelector() { @@ -102,6 +97,50 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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)] @@ -126,19 +165,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [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 this.textBox != null ? this.textBox.DownloadYahooUILibrary : this.downloadYuiLibrary; + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.DownloadYahooUILibrary; } set { - // 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. - if (this.textBox != null) { - this.textBox.DownloadYahooUILibrary = value; - } else { - this.downloadYuiLibrary = value; - } + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.DownloadYahooUILibrary = value; } } @@ -207,10 +240,37 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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; @@ -225,12 +285,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.textBox.ID = "openid_identifier"; this.textBox.HookFormSubmit = false; this.textBox.ShowLogOnPostBackButton = true; - this.textBox.DownloadYahooUILibrary = this.downloadYuiLibrary; - this.Controls.Add(this.textBox); this.positiveAssertionField = new HiddenField(); - this.positiveAssertionField.ID = this.ID + AuthDataFormKeySuffix; - this.Controls.Add(this.positiveAssertionField); } /// <summary> @@ -254,11 +310,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.EnsureValidButtons(); var css = new HtmlLink(); - 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 + 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); @@ -288,6 +349,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js index c58e06e..297ea23 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.js @@ -178,15 +178,19 @@ $(function() { 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(); - } else if (e.keyCode == $.ui.keyCode.ENTER) { - // we do NOT want to submit the form on ENTER. - e.preventDefault(); } }); });
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs index 4d635fb..335b435 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -4,7 +4,7 @@ // </copyright> //----------------------------------------------------------------------- -[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox.EmbeddedLogoResourceName, "image/gif")] +[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 @@ -45,7 +45,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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.gif"; + internal const string EmbeddedLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.openid_login.png"; /// <summary> /// Default value for <see cref="TabIndex"/> property. @@ -584,6 +584,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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); @@ -625,6 +627,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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) { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs index 6e7d7ef..5cfa191 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAnonymousResponse.cs @@ -26,7 +26,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Information about the OP endpoint that issued this assertion. /// </summary> - private readonly ProviderEndpointDescription provider; + private readonly IProviderEndpoint provider; /// <summary> /// Initializes a new instance of the <see cref="PositiveAnonymousResponse"/> class. diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index 695aa1e..3e2298c 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -28,7 +28,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { : base(response) { Contract.Requires<ArgumentNullException>(relyingParty != null); - this.Endpoint = ServiceEndpoint.CreateForClaimedIdentifier( + this.Endpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier( this.Response.ClaimedIdentifier, this.Response.GetReturnToArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName), this.Response.LocalIdentifier, @@ -114,7 +114,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// the claimed identifier to avoid a Provider asserting an Identifier /// for which it has no authority. /// </remarks> - internal ServiceEndpoint Endpoint { get; private set; } + internal IdentifierDiscoveryResult Endpoint { get; private set; } /// <summary> /// Gets the positive assertion response message. @@ -146,6 +146,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } + // 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 @@ -155,7 +163,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // 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 = claimedId.Discover(relyingParty.WebRequestHandler); + var discoveryResults = relyingParty.Discover(claimedId); ErrorUtilities.VerifyProtocol( discoveryResults.Contains(this.Endpoint), OpenIdStrings.IssuedAssertionFailsIdentifierDiscovery, diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs index ff29498..a7686c5 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs @@ -16,11 +16,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </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; } /// <summary> @@ -111,11 +118,51 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { 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 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<ServiceEndpoint> FilterEndpoints(IEnumerable<ServiceEndpoint> endpoints) { + internal IEnumerable<IdentifierDiscoveryResult> FilterEndpoints(IEnumerable<IdentifierDiscoveryResult> endpoints) { return endpoints .Where(se => !this.RejectDelegatingIdentifiers || se.ClaimedIdentifier == se.ProviderLocalIdentifier) .Where(se => !this.RequireDirectedIdentity || se.ClaimedIdentifier == se.Protocol.ClaimedIdentifierForOPIdentifier); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs index 15b6ca7..ac4dcbf 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Drawing.Design; @@ -24,6 +25,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs index 3a05287..2195e73 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Drawing.Design; @@ -25,6 +26,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <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> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs index 912b8f4..678f69a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SimpleXrdsProviderEndpoint.cs @@ -6,6 +6,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System; + using System.Collections.ObjectModel; + using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.Messages; /// <summary> @@ -13,7 +15,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// assertions (particularly unsolicited ones) are received from OP endpoints that /// are deemed permissible by the host RP. /// </summary> - internal class SimpleXrdsProviderEndpoint : IXrdsProviderEndpoint { + internal class SimpleXrdsProviderEndpoint : IProviderEndpoint { /// <summary> /// Initializes a new instance of the <see cref="SimpleXrdsProviderEndpoint"/> class. /// </summary> @@ -23,29 +25,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.Version = positiveAssertion.Version; } - #region IXrdsProviderEndpoint Properties - - /// <summary> - /// Gets the priority associated with this service that may have been given - /// in the XRDS document. - /// </summary> - public int? ServicePriority { - get { return null; } - } - - /// <summary> - /// Gets the priority associated with the service endpoint URL. - /// </summary> - /// <remarks> - /// When sorting by priority, this property should be considered second after - /// <see cref="ServicePriority"/>. - /// </remarks> - public int? UriPriority { - get { return null; } - } - - #endregion - #region IProviderEndpoint Members /// <summary> @@ -56,7 +35,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Gets the URL that the OpenID Provider receives authentication requests at. /// </summary> - /// <value></value> public Uri Uri { get; private set; } /// <summary> @@ -73,8 +51,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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<T>() where T : DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension, new() { - throw new NotSupportedException(); + bool IProviderEndpoint.IsExtensionSupported<T>() { + throw new NotImplementedException(); } /// <summary> @@ -91,23 +69,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// 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) { - throw new NotSupportedException(); - } - - #endregion - - #region IXrdsProviderEndpoint Methods - - /// <summary> - /// Checks for the presence of a given Type URI in an XRDS service. - /// </summary> - /// <param name="typeUri">The type URI to check for.</param> - /// <returns> - /// <c>true</c> if the service type uri is present; <c>false</c> otherwise. - /// </returns> - public bool IsTypeUriPresent(string typeUri) { - throw new NotSupportedException(); + bool IProviderEndpoint.IsExtensionSupported(Type extensionType) { + throw new NotImplementedException(); } #endregion diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.gif b/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.gif Binary files differdeleted file mode 100644 index cde836c..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.gif +++ /dev/null diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.png b/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.png Binary files differnew file mode 100644 index 0000000..caebd58 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.png diff --git a/src/DotNetOpenAuth/OpenId/UriDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/UriDiscoveryService.cs new file mode 100644 index 0000000..7d17fd9 --- /dev/null +++ b/src/DotNetOpenAuth/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/UriIdentifier.cs b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs index 28d8b37..639ff57 100644 --- a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs @@ -10,6 +10,9 @@ namespace DotNetOpenAuth.OpenId { 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; @@ -30,6 +33,68 @@ namespace DotNetOpenAuth.OpenId { 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> @@ -62,7 +127,8 @@ namespace DotNetOpenAuth.OpenId { /// 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) { + internal UriIdentifier(Uri uri) + : this(uri, false) { } /// <summary> @@ -73,7 +139,13 @@ namespace DotNetOpenAuth.OpenId { internal UriIdentifier(Uri uri, bool requireSslDiscovery) : base(uri != null ? uri.OriginalString : null, requireSslDiscovery) { Contract.Requires<ArgumentNullException>(uri != null); - if (!TryCanonicalize(new UriBuilder(uri), out uri)) { + + string uriAsString = uri.OriginalString; + if (schemeSubstitution) { + uriAsString = NormalSchemeToSpecialRoundTrippingScheme(uriAsString); + } + + if (!TryCanonicalize(uriAsString, out uri)) { throw new UriFormatException(); } if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) { @@ -96,6 +168,26 @@ namespace DotNetOpenAuth.OpenId { 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> @@ -137,7 +229,12 @@ namespace DotNetOpenAuth.OpenId { if (other == null) { return false; } - return this.Uri == other.Uri; + + if (this.ProblematicNormalization || other.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).Equals(new SimpleUri(other.OriginalString)); + } else { + return this.Uri == other.Uri; + } } /// <summary> @@ -157,16 +254,13 @@ namespace DotNetOpenAuth.OpenId { /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. /// </returns> public override string ToString() { - return Uri.AbsoluteUri; - } -#if UNUSED - static bool TryCanonicalize(string uri, out string canonicalUri) { - Uri normalizedUri; - bool result = TryCanonicalize(uri, out normalizedUri); - canonicalUri = normalizedUri.AbsoluteUri; - return result; + if (this.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).ToString(); + } else { + return this.Uri.AbsoluteUri; + } } -#endif + /// <summary> /// Determines whether a URI is a valid OpenID Identifier (of any kind). /// </summary> @@ -207,54 +301,6 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Performs discovery on the Identifier. - /// </summary> - /// <param name="requestHandler">The web request handler to use for discovery.</param> - /// <returns> - /// An initialized structure containing the discovered provider endpoint information. - /// </returns> - internal override IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler) { - List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>(); - - // Attempt YADIS discovery - DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this, IsDiscoverySecureEndToEnd); - if (yadisResult != null) { - if (yadisResult.IsXrds) { - try { - XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); - var xrdsEndpoints = xrds.CreateServiceEndpoints(yadisResult.NormalizedUri, this); - - // Filter out insecure endpoints if high security is required. - if (IsDiscoverySecureEndToEnd) { - xrdsEndpoints = xrdsEndpoints.Where(se => se.IsSecure); - } - 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) { - var htmlEndpoints = new List<ServiceEndpoint>(DiscoverFromHtml(yadisResult.NormalizedUri, this, 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 => !IsDiscoverySecureEndToEnd || ep.IsSecure)); - 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; - } - - /// <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. @@ -270,9 +316,7 @@ namespace DotNetOpenAuth.OpenId { } // Strip the fragment. - UriBuilder builder = new UriBuilder(Uri); - builder.Fragment = null; - return builder.Uri; + return new UriIdentifier(this.OriginalString.Substring(0, this.OriginalString.IndexOf('#'))); } /// <summary> @@ -318,54 +362,6 @@ namespace DotNetOpenAuth.OpenId { } /// <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<ServiceEndpoint> 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 ServiceEndpoint.CreateForClaimedIdentifier( - claimedIdentifier, - userSuppliedIdentifier, - providerLocalIdentifier, - new ProviderEndpointDescription(providerEndpoint, typeURIs), - (int?)null, - (int?)null); - } - } - } - - /// <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> @@ -431,8 +427,12 @@ namespace DotNetOpenAuth.OpenId { schemePrepended = true; } + if (schemeSubstitution) { + uri = NormalSchemeToSpecialRoundTrippingScheme(uri); + } + // Use a UriBuilder because it helps to normalize the URL as well. - return TryCanonicalize(new UriBuilder(uri), out canonicalUri); + return TryCanonicalize(uri, out canonicalUri); } catch (UriFormatException) { // We try not to land here with checks in the try block, but just in case. return false; @@ -440,9 +440,9 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Removes the fragment from a URL and sets the host to lowercase. + /// Fixes up the scheme if appropriate. /// </summary> - /// <param name="uriBuilder">The URI builder with the value to canonicalize.</param> + /// <param name="uri">The URI to canonicalize.</param> /// <param name="canonicalUri">The resulting canonical URI.</param> /// <returns><c>true</c> if the canonicalization was successful; <c>false</c> otherwise.</returns> /// <remarks> @@ -452,12 +452,48 @@ namespace DotNetOpenAuth.OpenId { /// 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(UriBuilder uriBuilder, out Uri canonicalUri) { - uriBuilder.Host = uriBuilder.Host.ToLowerInvariant(); - canonicalUri = uriBuilder.Uri; + 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); + 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); + } + #if CONTRACTS_FULL /// <summary> /// Verifies conditions that should be true for any valid state of this object. @@ -470,5 +506,193 @@ namespace DotNetOpenAuth.OpenId { 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)); + + // 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/XriDiscoveryProxyService.cs b/src/DotNetOpenAuth/OpenId/XriDiscoveryProxyService.cs new file mode 100644 index 0000000..b1a3430 --- /dev/null +++ b/src/DotNetOpenAuth/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/XriIdentifier.cs b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs index baba2e5..729f603 100644 --- a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs @@ -35,23 +35,6 @@ namespace DotNetOpenAuth.OpenId { private const string XriScheme = "xri://"; /// <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> - /// The XRI proxy resolver to use for finding XRDS documents from an XRI. - /// </summary> - private readonly string xriResolverProxy; - - /// <summary> /// Backing store for the <see cref="CanonicalXri"/> property. /// </summary> private readonly string canonicalXri; @@ -79,12 +62,6 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(xri)); Contract.Requires<FormatException>(IsValidXri(xri), OpenIdStrings.InvalidXri); Contract.Assume(xri != null); // Proven by IsValidXri - this.xriResolverProxy = XriResolverProxyTemplate; - if (requireSsl) { - // Indicate to xri.net that we require SSL to be used for delegated resolution - // of community i-names. - this.xriResolverProxy += ";https=true"; - } this.OriginalXri = xri; this.canonicalXri = CanonicalizeXri(xri); } @@ -105,21 +82,6 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Gets the URL from which this XRI's XRDS document may be downloaded. - /// </summary> - private Uri XrdsUrl { - get { - ErrorUtilities.VerifyProtocol(DotNetOpenAuthSection.Configuration.OpenId.XriResolver.Enabled, OpenIdStrings.XriResolutionDisabled); - return new Uri( - string.Format( - CultureInfo.InvariantCulture, - this.xriResolverProxy, - this, - DotNetOpenAuthSection.Configuration.OpenId.XriResolver.Proxy.Name)); - } - } - - /// <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> @@ -180,30 +142,6 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Performs discovery on the Identifier. - /// </summary> - /// <param name="requestHandler">The web request handler to use for discovery.</param> - /// <returns> - /// An initialized structure containing the discovered provider endpoint information. - /// </returns> - internal override IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler) { - return this.DownloadXrds(requestHandler).CreateServiceEndpoints(this); - } - - /// <summary> - /// Performs discovery on THIS identifier, but generates <see cref="ServiceEndpoint"/> - /// instances that treat another given identifier as the user-supplied identifier. - /// </summary> - /// <param name="requestHandler">The request handler to use in discovery.</param> - /// <param name="userSuppliedIdentifier">The user supplied identifier, which may differ from this XRI instance due to multiple discovery steps.</param> - /// <returns>A list of service endpoints offered for this identifier.</returns> - internal IEnumerable<ServiceEndpoint> Discover(IDirectWebRequestHandler requestHandler, XriIdentifier userSuppliedIdentifier) { - Contract.Requires<ArgumentNullException>(requestHandler != null); - Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null); - return this.DownloadXrds(requestHandler).CreateServiceEndpoints(userSuppliedIdentifier); - } - - /// <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. @@ -255,22 +193,6 @@ namespace DotNetOpenAuth.OpenId { return xri; } - /// <summary> - /// Downloads the XRDS document for this XRI. - /// </summary> - /// <param name="requestHandler">The request handler.</param> - /// <returns>The XRDS document.</returns> - private XrdsDocument DownloadXrds(IDirectWebRequestHandler requestHandler) { - Contract.Requires<ArgumentNullException>(requestHandler != null); - Contract.Ensures(Contract.Result<XrdsDocument>() != null); - XrdsDocument doc; - using (var xrdsResponse = Yadis.Request(requestHandler, this.XrdsUrl, this.IsDiscoverySecureEndToEnd)) { - doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream)); - } - ErrorUtilities.VerifyProtocol(doc.IsXrdResolutionSuccessful, OpenIdStrings.XriResolutionFailed); - return doc; - } - #if CONTRACTS_FULL /// <summary> /// Verifies conditions that should be true for any valid state of this object. @@ -279,7 +201,6 @@ namespace DotNetOpenAuth.OpenId { [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] [ContractInvariantMethod] private void ObjectInvariant() { - Contract.Invariant(this.xriResolverProxy != null); Contract.Invariant(this.canonicalXri != null); } #endif diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs index 2235986..612845f 100644 --- a/src/DotNetOpenAuth/Reporting.cs +++ b/src/DotNetOpenAuth/Reporting.cs @@ -30,7 +30,27 @@ namespace DotNetOpenAuth { /// The statistical reporting mechanism used so this library's project authors /// know what versions and features are in use. /// </summary> - internal static class Reporting { + public static class Reporting { + /// <summary> + /// A value indicating whether reporting is desirable or not. Must be logical-AND'd with !<see cref="broken"/>. + /// </summary> + private static bool enabled; + + /// <summary> + /// A value indicating whether reporting experienced an error and cannot be enabled. + /// </summary> + private static bool broken; + + /// <summary> + /// A value indicating whether the reporting class has been initialized or not. + /// </summary> + private static bool initialized; + + /// <summary> + /// The object to lock during initialization. + /// </summary> + private static object initializationSync = new object(); + /// <summary> /// The isolated storage to use for collecting data in between published reports. /// </summary> @@ -93,37 +113,31 @@ namespace DotNetOpenAuth { [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Reporting MUST NOT cause unhandled exceptions.")] static Reporting() { Enabled = DotNetOpenAuthSection.Configuration.Reporting.Enabled; - if (Enabled) { - try { - file = GetIsolatedStorage(); - reportOriginIdentity = GetOrCreateOriginIdentity(); - - webRequestHandler = new StandardWebRequestHandler(); - observations.Add(observedRequests = new PersistentHashSet(file, "requests.txt", 3)); - observations.Add(observedCultures = new PersistentHashSet(file, "cultures.txt", 20)); - observations.Add(observedFeatures = new PersistentHashSet(file, "features.txt", int.MaxValue)); - - // Record site-wide features in use. - if (HttpContext.Current != null && HttpContext.Current.ApplicationInstance != null) { - // MVC or web forms? - // front-end or back end web farm? - // url rewriting? - ////RecordFeatureUse(IsMVC ? "ASP.NET MVC" : "ASP.NET Web Forms"); - } - } catch (Exception e) { - // This is supposed to be as low-risk as possible, so if it fails, just disable reporting - // and avoid rethrowing. - Enabled = false; - Logger.Library.Error("Error while trying to initialize reporting.", e); - } - } } /// <summary> /// Gets or sets a value indicating whether this reporting is enabled. /// </summary> /// <value><c>true</c> if enabled; otherwise, <c>false</c>.</value> - internal static bool Enabled { get; set; } + /// <remarks> + /// Setting this property to <c>true</c> <i>may</i> have no effect + /// if reporting has already experienced a failure of some kind. + /// </remarks> + public static bool Enabled { + get { + return enabled && !broken; + } + + set { + if (value) { + Initialize(); + } + + // Only set the static field here, so that other threads + // don't try to use reporting while we're initializing it. + enabled = value; + } + } /// <summary> /// Gets the configuration to use for reporting. @@ -137,6 +151,7 @@ namespace DotNetOpenAuth { /// </summary> /// <param name="eventName">Name of the event.</param> /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "PersistentCounter instances are stored in a table for later use.")] internal static void RecordEventOccurrence(string eventName, string category) { Contract.Requires(!String.IsNullOrEmpty(eventName)); @@ -302,49 +317,89 @@ namespace DotNetOpenAuth { } /// <summary> + /// Initializes Reporting if it has not been initialized yet. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method must never throw.")] + private static void Initialize() { + lock (initializationSync) { + if (!broken && !initialized) { + try { + file = GetIsolatedStorage(); + reportOriginIdentity = GetOrCreateOriginIdentity(); + + webRequestHandler = new StandardWebRequestHandler(); + observations.Add(observedRequests = new PersistentHashSet(file, "requests.txt", 3)); + observations.Add(observedCultures = new PersistentHashSet(file, "cultures.txt", 20)); + observations.Add(observedFeatures = new PersistentHashSet(file, "features.txt", int.MaxValue)); + + // Record site-wide features in use. + if (HttpContext.Current != null && HttpContext.Current.ApplicationInstance != null) { + // MVC or web forms? + // front-end or back end web farm? + // url rewriting? + ////RecordFeatureUse(IsMVC ? "ASP.NET MVC" : "ASP.NET Web Forms"); + } + + initialized = true; + } catch (Exception e) { + // This is supposed to be as low-risk as possible, so if it fails, just disable reporting + // and avoid rethrowing. + broken = true; + Logger.Library.Error("Error while trying to initialize reporting.", e); + } + } + } + } + + /// <summary> /// Assembles a report for submission. /// </summary> /// <returns>A stream that contains the report.</returns> private static Stream GetReport() { var stream = new MemoryStream(); - var writer = new StreamWriter(stream, Encoding.UTF8); - writer.WriteLine(reportOriginIdentity.ToString("B")); - writer.WriteLine(Util.LibraryVersion); - writer.WriteLine(".NET Framework {0}", Environment.Version); - - foreach (var observation in observations) { - observation.Flush(); - writer.WriteLine("===================================="); - writer.WriteLine(observation.FileName); - try { - using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { - writer.Flush(); - fileStream.CopyTo(writer.BaseStream); + try { + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine(reportOriginIdentity.ToString("B")); + writer.WriteLine(Util.LibraryVersion); + writer.WriteLine(".NET Framework {0}", Environment.Version); + + foreach (var observation in observations) { + observation.Flush(); + writer.WriteLine("===================================="); + writer.WriteLine(observation.FileName); + try { + using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } + } catch (FileNotFoundException) { + writer.WriteLine("(missing)"); } - } catch (FileNotFoundException) { - writer.WriteLine("(missing)"); } - } - // Not all event counters may have even loaded in this app instance. - // We flush the ones in memory, and then read all of them off disk. - foreach (var counter in events.Values) { - counter.Flush(); - } + // Not all event counters may have even loaded in this app instance. + // We flush the ones in memory, and then read all of them off disk. + foreach (var counter in events.Values) { + counter.Flush(); + } - foreach (string eventFile in file.GetFileNames("event-*.txt")) { - writer.WriteLine("===================================="); - writer.WriteLine(eventFile); - using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { - writer.Flush(); - fileStream.CopyTo(writer.BaseStream); + foreach (string eventFile in file.GetFileNames("event-*.txt")) { + writer.WriteLine("===================================="); + writer.WriteLine(eventFile); + using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } } - } - // Make sure the stream is positioned at the beginning. - writer.Flush(); - stream.Position = 0; - return stream; + // Make sure the stream is positioned at the beginning. + writer.Flush(); + stream.Position = 0; + return stream; + } catch { + stream.Dispose(); + throw; + } } /// <summary> @@ -459,7 +514,7 @@ namespace DotNetOpenAuth { } catch (Exception ex) { // Something bad and unexpected happened. Just deactivate to avoid more trouble. Logger.Library.Error("Error while trying to submit statistical report.", ex); - Enabled = false; + broken = true; } }); } diff --git a/src/DotNetOpenAuth/Strings.Designer.cs b/src/DotNetOpenAuth/Strings.Designer.cs index 38c89f7..70b9fb2 100644 --- a/src/DotNetOpenAuth/Strings.Designer.cs +++ b/src/DotNetOpenAuth/Strings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.4927 +// Runtime Version:4.0.30104.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth { // 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", "2.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Strings { @@ -70,20 +70,20 @@ namespace DotNetOpenAuth { } /// <summary> - /// Looks up a localized string similar to No current HttpContext was detected, so an {0} instance must be explicitly provided or specified in the .config file. Call the constructor overload that takes an {0}.. + /// Looks up a localized string similar to The configuration XAML reference to {0} requires a current HttpContext to resolve.. /// </summary> - internal static string StoreRequiredWhenNoHttpContextAvailable { + internal static string ConfigurationXamlReferenceRequiresHttpContext { get { - return ResourceManager.GetString("StoreRequiredWhenNoHttpContextAvailable", resourceCulture); + return ResourceManager.GetString("ConfigurationXamlReferenceRequiresHttpContext", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to The configuration XAML reference to {0} requires a current HttpContext to resolve.. + /// Looks up a localized string similar to No current HttpContext was detected, so an {0} instance must be explicitly provided or specified in the .config file. Call the constructor overload that takes an {0}.. /// </summary> - internal static string ConfigurationXamlReferenceRequiresHttpContext { + internal static string StoreRequiredWhenNoHttpContextAvailable { get { - return ResourceManager.GetString("ConfigurationXamlReferenceRequiresHttpContext", resourceCulture); + return ResourceManager.GetString("StoreRequiredWhenNoHttpContextAvailable", resourceCulture); } } } diff --git a/src/DotNetOpenAuth/Util.cs b/src/DotNetOpenAuth/Util.cs index 9f8b30c..8a18ef8 100644 --- a/src/DotNetOpenAuth/Util.cs +++ b/src/DotNetOpenAuth/Util.cs @@ -126,7 +126,7 @@ namespace DotNetOpenAuth { sb.Append("\t"); sb.Append(objString); - if (!objString.EndsWith(Environment.NewLine)) { + if (!objString.EndsWith(Environment.NewLine, StringComparison.Ordinal)) { sb.AppendLine(); } sb.AppendLine("}, {"); diff --git a/src/DotNetOpenAuth/Xrds/XrdElement.cs b/src/DotNetOpenAuth/Xrds/XrdElement.cs index a8cc145..2cdc720 100644 --- a/src/DotNetOpenAuth/Xrds/XrdElement.cs +++ b/src/DotNetOpenAuth/Xrds/XrdElement.cs @@ -133,7 +133,7 @@ namespace DotNetOpenAuth.Xrds { /// </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> - private IEnumerable<ServiceElement> SearchForServiceTypeUris(Func<Protocol, string> p) { + internal IEnumerable<ServiceElement> SearchForServiceTypeUris(Func<Protocol, string> p) { var xpath = new StringBuilder(); xpath.Append("xrd:Service["); foreach (var protocol in Protocol.AllVersions) { diff --git a/src/DotNetOpenAuth/Xrds/XrdsDocument.cs b/src/DotNetOpenAuth/Xrds/XrdsDocument.cs index dd0e19f..e2c2d72 100644 --- a/src/DotNetOpenAuth/Xrds/XrdsDocument.cs +++ b/src/DotNetOpenAuth/Xrds/XrdsDocument.cs @@ -18,6 +18,16 @@ namespace DotNetOpenAuth.Xrds { /// </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> @@ -26,6 +36,8 @@ namespace DotNetOpenAuth.Xrds { 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> diff --git a/src/DotNetOpenAuth/Xrds/XrdsNode.cs b/src/DotNetOpenAuth/Xrds/XrdsNode.cs index f8fa0af..39bd9b9 100644 --- a/src/DotNetOpenAuth/Xrds/XrdsNode.cs +++ b/src/DotNetOpenAuth/Xrds/XrdsNode.cs @@ -54,16 +54,16 @@ namespace DotNetOpenAuth.Xrds { /// <summary> /// Gets the node. /// </summary> - protected XPathNavigator Node { get; private set; } + internal XPathNavigator Node { get; private set; } /// <summary> /// Gets the parent node, or null if this is the root node. /// </summary> - protected XrdsNode ParentNode { get; private set; } + protected internal XrdsNode ParentNode { get; private set; } /// <summary> /// Gets the XML namespace resolver to use in XPath expressions. /// </summary> - protected XmlNamespaceManager XmlNamespaceResolver { get; private set; } + protected internal XmlNamespaceManager XmlNamespaceResolver { get; private set; } } } diff --git a/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs b/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs index 465c990..2279b5f 100644 --- a/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs +++ b/src/DotNetOpenAuth/Xrds/XrdsStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.3053 +// Runtime Version:4.0.30104.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -19,7 +19,7 @@ namespace DotNetOpenAuth.Xrds { // 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", "2.0.0.0")] + [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 { diff --git a/src/DotNetOpenAuth/XrdsPublisher.cs b/src/DotNetOpenAuth/XrdsPublisher.cs index e7c04d8..83d82ff 100644 --- a/src/DotNetOpenAuth/XrdsPublisher.cs +++ b/src/DotNetOpenAuth/XrdsPublisher.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth { 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; @@ -209,6 +210,7 @@ namespace DotNetOpenAuth { /// <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) { + 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) { diff --git a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs index 01dae40..06c6fc7 100644 --- a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs +++ b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs @@ -17,6 +17,12 @@ namespace DotNetOpenAuth.Yadis { /// </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> @@ -25,10 +31,8 @@ namespace DotNetOpenAuth.Yadis { public DiscoveryResult(Uri requestUri, CachedDirectWebResponse initialResponse, CachedDirectWebResponse finalResponse) { this.RequestUri = requestUri; this.NormalizedUri = initialResponse.FinalUri; - if (finalResponse == null) { - this.ContentType = initialResponse.ContentType; - this.ResponseText = initialResponse.GetResponseString(); - this.IsXrds = this.ContentType != null && this.ContentType.MediaType == ContentTypes.Xrds; + if (finalResponse == null || finalResponse.Status != System.Net.HttpStatusCode.OK) { + this.ApplyHtmlResponse(initialResponse); } else { this.ContentType = finalResponse.ContentType; this.ResponseText = finalResponse.GetResponseString(); @@ -36,6 +40,9 @@ namespace DotNetOpenAuth.Yadis { if (initialResponse != finalResponse) { this.YadisLocation = finalResponse.RequestUri; } + + // Back up the initial HTML response in case the XRDS is not useful. + this.htmlFallback = initialResponse; } } @@ -77,13 +84,23 @@ namespace DotNetOpenAuth.Yadis { public bool IsXrds { get; private set; } /// <summary> - /// Gets a value indicating whether discovery resulted in an - /// XRDS document at a referred location. + /// 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> - /// <value><c>true</c> if the response to the userSuppliedIdentifier - /// pointed to a different URL for the XRDS document.</value> - public bool UsedYadisLocation { - get { return this.YadisLocation != null; } + /// <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/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs index f1c8be3..8b8c20f 100644 --- a/src/DotNetOpenAuth/Yadis/Yadis.cs +++ b/src/DotNetOpenAuth/Yadis/Yadis.cs @@ -39,7 +39,7 @@ namespace DotNetOpenAuth.Yadis { /// The maximum number of bytes to read from an HTTP response /// in searching for a link to a YADIS document. /// </summary> - private const int MaximumResultToScan = 1024 * 1024; + internal const int MaximumResultToScan = 1024 * 1024; /// <summary> /// Performs YADIS discovery on some identifier. @@ -96,7 +96,6 @@ namespace DotNetOpenAuth.Yadis { 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); - return null; } } else { Logger.Yadis.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url); |