diff options
Diffstat (limited to 'src/DotNetOpenAuth.OAuth')
58 files changed, 6592 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OAuth/Configuration/OAuthConsumerElement.cs b/src/DotNetOpenAuth.OAuth/Configuration/OAuthConsumerElement.cs new file mode 100644 index 0000000..b15c3e3 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/Configuration/OAuthConsumerElement.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthConsumerElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + + /// <summary> + /// Represents the <oauth/consumer> element in the host's .config file. + /// </summary> + internal class OAuthConsumerElement : ConfigurationElement { + /// <summary> + /// Gets the name of the security sub-element. + /// </summary> + private const string SecuritySettingsConfigName = "security"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthConsumerElement"/> class. + /// </summary> + internal OAuthConsumerElement() { + } + + /// <summary> + /// Gets or sets the security settings. + /// </summary> + [ConfigurationProperty(SecuritySettingsConfigName)] + public OAuthConsumerSecuritySettingsElement SecuritySettings { + get { return (OAuthConsumerSecuritySettingsElement)this[SecuritySettingsConfigName] ?? new OAuthConsumerSecuritySettingsElement(); } + set { this[SecuritySettingsConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/Configuration/OAuthConsumerSecuritySettingsElement.cs b/src/DotNetOpenAuth.OAuth/Configuration/OAuthConsumerSecuritySettingsElement.cs new file mode 100644 index 0000000..38a183a --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/Configuration/OAuthConsumerSecuritySettingsElement.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthConsumerSecuritySettingsElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth; + + /// <summary> + /// Security settings that are applicable to consumers. + /// </summary> + internal class OAuthConsumerSecuritySettingsElement : ConfigurationElement { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthConsumerSecuritySettingsElement"/> class. + /// </summary> + internal OAuthConsumerSecuritySettingsElement() { + } + + /// <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> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "By design")] + internal ConsumerSecuritySettings CreateSecuritySettings() { + return new ConsumerSecuritySettings(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/Configuration/OAuthElement.cs b/src/DotNetOpenAuth.OAuth/Configuration/OAuthElement.cs new file mode 100644 index 0000000..282bdba --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/Configuration/OAuthElement.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + + /// <summary> + /// Represents the <oauth> element in the host's .config file. + /// </summary> + internal class OAuthElement : ConfigurationElement { + /// <summary> + /// The name of the <consumer> sub-element. + /// </summary> + private const string ConsumerElementName = "consumer"; + + /// <summary> + /// The name of the <serviceProvider> sub-element. + /// </summary> + private const string ServiceProviderElementName = "serviceProvider"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthElement"/> class. + /// </summary> + internal OAuthElement() { + } + + /// <summary> + /// Gets or sets the configuration specific for Consumers. + /// </summary> + [ConfigurationProperty(ConsumerElementName)] + internal OAuthConsumerElement Consumer { + get { return (OAuthConsumerElement)this[ConsumerElementName] ?? new OAuthConsumerElement(); } + set { this[ConsumerElementName] = value; } + } + + /// <summary> + /// Gets or sets the configuration specific for Service Providers. + /// </summary> + [ConfigurationProperty(ServiceProviderElementName)] + internal OAuthServiceProviderElement ServiceProvider { + get { return (OAuthServiceProviderElement)this[ServiceProviderElementName] ?? new OAuthServiceProviderElement(); } + set { this[ServiceProviderElementName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/Configuration/OAuthServiceProviderElement.cs b/src/DotNetOpenAuth.OAuth/Configuration/OAuthServiceProviderElement.cs new file mode 100644 index 0000000..8e910a0 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/Configuration/OAuthServiceProviderElement.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthServiceProviderElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// Represents the <oauth/serviceProvider> element in the host's .config file. + /// </summary> + internal class OAuthServiceProviderElement : ConfigurationElement { + /// <summary> + /// The name of the custom store sub-element. + /// </summary> + private const string StoreConfigName = "store"; + + /// <summary> + /// Gets the name of the security sub-element. + /// </summary> + private const string SecuritySettingsConfigName = "security"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthServiceProviderElement"/> class. + /// </summary> + internal OAuthServiceProviderElement() { + } + + /// <summary> + /// Gets or sets the type to use for storing application state. + /// </summary> + [ConfigurationProperty(StoreConfigName)] + public TypeConfigurationElement<INonceStore> ApplicationStore { + get { return (TypeConfigurationElement<INonceStore>)this[StoreConfigName] ?? new TypeConfigurationElement<INonceStore>(); } + set { this[StoreConfigName] = value; } + } + + /// <summary> + /// Gets or sets the security settings. + /// </summary> + [ConfigurationProperty(SecuritySettingsConfigName)] + public OAuthServiceProviderSecuritySettingsElement SecuritySettings { + get { return (OAuthServiceProviderSecuritySettingsElement)this[SecuritySettingsConfigName] ?? new OAuthServiceProviderSecuritySettingsElement(); } + set { this[SecuritySettingsConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/Configuration/OAuthServiceProviderSecuritySettingsElement.cs b/src/DotNetOpenAuth.OAuth/Configuration/OAuthServiceProviderSecuritySettingsElement.cs new file mode 100644 index 0000000..723b607 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/Configuration/OAuthServiceProviderSecuritySettingsElement.cs @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthServiceProviderSecuritySettingsElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth; + + /// <summary> + /// Security settings that are applicable to service providers. + /// </summary> + internal class OAuthServiceProviderSecuritySettingsElement : ConfigurationElement { + /// <summary> + /// Gets the name of the @minimumRequiredOAuthVersion attribute. + /// </summary> + private const string MinimumRequiredOAuthVersionConfigName = "minimumRequiredOAuthVersion"; + + /// <summary> + /// Gets the name of the @maxAuthorizationTime attribute. + /// </summary> + private const string MaximumRequestTokenTimeToLiveConfigName = "maxAuthorizationTime"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthServiceProviderSecuritySettingsElement"/> class. + /// </summary> + internal OAuthServiceProviderSecuritySettingsElement() { + } + + /// <summary> + /// Gets or sets the minimum OAuth version a Consumer is required to support in order for this library to interoperate with it. + /// </summary> + /// <remarks> + /// Although the earliest versions of OAuth are supported, for security reasons it may be desirable to require the + /// remote party to support a later version of OAuth. + /// </remarks> + [ConfigurationProperty(MinimumRequiredOAuthVersionConfigName, DefaultValue = "V10")] + public ProtocolVersion MinimumRequiredOAuthVersion { + get { return (ProtocolVersion)this[MinimumRequiredOAuthVersionConfigName]; } + set { this[MinimumRequiredOAuthVersionConfigName] = value; } + } + + /// <summary> + /// Gets or sets the maximum time a user can take to complete authorization. + /// </summary> + /// <remarks> + /// This time limit serves as a security mitigation against brute force attacks to + /// compromise (unauthorized or authorized) request tokens. + /// Longer time limits is more friendly to slow users or consumers, while shorter + /// time limits provide better security. + /// </remarks> + [ConfigurationProperty(MaximumRequestTokenTimeToLiveConfigName, DefaultValue = "0:05")] // 5 minutes + [PositiveTimeSpanValidator] + public TimeSpan MaximumRequestTokenTimeToLive { + get { return (TimeSpan)this[MaximumRequestTokenTimeToLiveConfigName]; } + set { this[MaximumRequestTokenTimeToLiveConfigName] = 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> + internal ServiceProviderSecuritySettings CreateSecuritySettings() { + return new ServiceProviderSecuritySettings { + MinimumRequiredOAuthVersion = this.MinimumRequiredOAuthVersion, + MaximumRequestTokenTimeToLive = this.MaximumRequestTokenTimeToLive, + }; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/DotNetOpenAuth.OAuth.csproj b/src/DotNetOpenAuth.OAuth/DotNetOpenAuth.OAuth.csproj new file mode 100644 index 0000000..8f1d474 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/DotNetOpenAuth.OAuth.csproj @@ -0,0 +1,389 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))\EnlistmentInfo.props" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))' != '' " /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <CodeContractsAssemblyMode>1</CodeContractsAssemblyMode> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <PropertyGroup> + <ProductVersion>9.0.30729</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{A288FCC8-6FCF-46DA-A45E-5F9281556361}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth</RootNamespace> + <AssemblyName>DotNetOpenAuth.OAuth</AssemblyName> + <AssemblyName Condition=" '$(NoUIControls)' == 'true' ">DotNetOpenAuth.NoUI</AssemblyName> + <FileAlignment>512</FileAlignment> + <StandardCopyright> +Copyright (c) 2009, Andrew Arnott. All rights reserved. +Code licensed under the Ms-PL License: +http://opensource.org/licenses/ms-pl.html +</StandardCopyright> + <FileUpgradeFlags> + </FileUpgradeFlags> + <OldToolsVersion>3.5</OldToolsVersion> + <UpgradeBackupLocation /> + <IsWebBootstrapper>false</IsWebBootstrapper> + <TargetFrameworkProfile /> + <PublishUrl>publish\</PublishUrl> + <Install>true</Install> + <InstallFrom>Disk</InstallFrom> + <UpdateEnabled>false</UpdateEnabled> + <UpdateMode>Foreground</UpdateMode> + <UpdateInterval>7</UpdateInterval> + <UpdateIntervalUnits>Days</UpdateIntervalUnits> + <UpdatePeriodically>false</UpdatePeriodically> + <UpdateRequired>false</UpdateRequired> + <MapFileExtensions>true</MapFileExtensions> + <ApplicationRevision>0</ApplicationRevision> + <ApplicationVersion>1.0.0.%2a</ApplicationVersion> + <UseApplicationTrust>false</UseApplicationTrust> + <BootstrapperEnabled>true</BootstrapperEnabled> + <ApplicationIcon> + </ApplicationIcon> + <DocumentationFile>$(OutputPath)$(AssemblyName).xml</DocumentationFile> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>false</AllowUnsafeBlocks> + <RunCodeAnalysis>false</RunCodeAnalysis> + <CodeAnalysisRules> + </CodeAnalysisRules> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>Full</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>False</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>True</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>True</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>True</CodeContractsShowSquigglies> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + <CodeContractsExtraRewriteOptions /> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <AllowUnsafeBlocks>false</AllowUnsafeBlocks> + <RunCodeAnalysis>true</RunCodeAnalysis> + <CodeAnalysisRules> + </CodeAnalysisRules> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>ReleaseRequires</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>False</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>False</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>False</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>False</CodeContractsShowSquigglies> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + <CodeContractsExtraRewriteOptions /> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'ReleaseNoUI|AnyCPU'"> + <DefineConstants>TRACE;NoUIControls</DefineConstants> + <NoUIControls>true</NoUIControls> + <Optimize>true</Optimize> + <NoWarn>;1607</NoWarn> + <DebugType>pdbonly</DebugType> + <PlatformTarget>AnyCPU</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>ReleaseRequires</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>False</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>False</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>False</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>False</CodeContractsShowSquigglies> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeContractsExtraRewriteOptions /> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeAnalysis|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DefineConstants>$(DefineConstants);CONTRACTS_FULL;DEBUG;TRACE</DefineConstants> + <DebugType>full</DebugType> + <PlatformTarget>AnyCPU</PlatformTarget> + <CodeAnalysisRules> + </CodeAnalysisRules> + <CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression> + <CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile> + <ErrorReport>prompt</ErrorReport> + <CodeContractsEnableRuntimeChecking>True</CodeContractsEnableRuntimeChecking> + <CodeContractsCustomRewriterAssembly> + </CodeContractsCustomRewriterAssembly> + <CodeContractsCustomRewriterClass> + </CodeContractsCustomRewriterClass> + <CodeContractsRuntimeCheckingLevel>Preconditions</CodeContractsRuntimeCheckingLevel> + <CodeContractsRunCodeAnalysis>True</CodeContractsRunCodeAnalysis> + <CodeContractsBuildReferenceAssembly>True</CodeContractsBuildReferenceAssembly> + <CodeContractsNonNullObligations>False</CodeContractsNonNullObligations> + <CodeContractsBoundsObligations>False</CodeContractsBoundsObligations> + <CodeContractsLibPaths> + </CodeContractsLibPaths> + <CodeContractsPlatformPath> + </CodeContractsPlatformPath> + <CodeContractsExtraAnalysisOptions> + </CodeContractsExtraAnalysisOptions> + <CodeContractsBaseLineFile> + </CodeContractsBaseLineFile> + <CodeContractsUseBaseLine>False</CodeContractsUseBaseLine> + <CodeContractsRunInBackground>True</CodeContractsRunInBackground> + <CodeContractsShowSquigglies>True</CodeContractsShowSquigglies> + <RunCodeAnalysis>true</RunCodeAnalysis> + <CodeContractsArithmeticObligations>False</CodeContractsArithmeticObligations> + <CodeContractsRuntimeOnlyPublicSurface>False</CodeContractsRuntimeOnlyPublicSurface> + <CodeContractsRuntimeThrowOnFailure>True</CodeContractsRuntimeThrowOnFailure> + <CodeContractsRuntimeCallSiteRequires>False</CodeContractsRuntimeCallSiteRequires> + <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> + <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> + <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> + <CodeContractsExtraRewriteOptions /> + </PropertyGroup> + <ItemGroup> + <Reference Include="log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + </Reference> + <Reference Include="PresentationFramework"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Security" /> + <Reference Include="System.configuration" /> + <Reference Include="System.Core"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Data" /> + <Reference Include="System.Drawing" /> + <Reference Include="System.IdentityModel"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.IdentityModel.Selectors"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.Runtime.Serialization"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.ServiceModel"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.ServiceModel.Web"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web" /> + <Reference Include="System.Web.Abstractions"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web.Extensions"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web.Extensions.Design"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Web.Mobile" Condition=" '$(ClrVersion)' != '4' " /> + <Reference Include="System.Web.Routing"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="System.Windows.Forms" /> + <Reference Include="System.Xaml" Condition=" '$(ClrVersion)' == '4' " /> + <Reference Include="System.XML" /> + <Reference Include="System.Xml.Linq"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + <Reference Include="WindowsBase"> + <RequiredTargetFramework>3.0</RequiredTargetFramework> + </Reference> + <Reference Include="System.ComponentModel.DataAnnotations"> + <RequiredTargetFramework>3.5</RequiredTargetFramework> + </Reference> + </ItemGroup> + <ItemGroup Condition=" '$(ClrVersion)' == '4' "> + <Reference Include="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + </ItemGroup> + <ItemGroup Condition=" '$(ClrVersion)' != '4' "> + <!-- MVC 2 can run on CLR 2 (it doesn't require CLR 4) but since MVC 2 apps tend to use type forwarding, + it's a more broadly consumable idea to bind against MVC 1 for the library unless we're building on CLR 4, + which will definitely have MVC 2 available. --> + <Reference Include="System.Web.Mvc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" /> + </ItemGroup> + <ItemGroup> + <Compile Include="Configuration\OAuthConsumerElement.cs" /> + <Compile Include="Configuration\OAuthConsumerSecuritySettingsElement.cs" /> + <Compile Include="Configuration\OAuthElement.cs" /> + <Compile Include="Configuration\OAuthServiceProviderElement.cs" /> + <Compile Include="Configuration\OAuthServiceProviderSecuritySettingsElement.cs" /> + <Compile Include="OAuth\ChannelElements\ICombinedOpenIdProviderTokenManager.cs" /> + <Compile Include="OAuth\ChannelElements\IConsumerDescription.cs" /> + <Compile Include="OAuth\ChannelElements\IConsumerTokenManager.cs" /> + <Compile Include="OAuth\ChannelElements\IOpenIdOAuthTokenManager.cs" /> + <Compile Include="OAuth\ChannelElements\IServiceProviderAccessToken.cs" /> + <Compile Include="OAuth\ChannelElements\IServiceProviderTokenManager.cs" /> + <Compile Include="OAuth\ChannelElements\OAuthConsumerMessageFactory.cs" /> + <Compile Include="OAuth\ChannelElements\ITokenGenerator.cs" /> + <Compile Include="OAuth\ChannelElements\ITokenManager.cs" /> + <Compile Include="OAuth\ChannelElements\OAuthHttpMethodBindingElement.cs" /> + <Compile Include="OAuth\ChannelElements\OAuthIdentity.cs" /> + <Compile Include="OAuth\ChannelElements\OAuthPrincipal.cs" /> + <Compile Include="OAuth\ChannelElements\PlaintextSigningBindingElement.cs" /> + <Compile Include="OAuth\ChannelElements\HmacSha1SigningBindingElement.cs" /> + <Compile Include="OAuth\ChannelElements\IServiceProviderRequestToken.cs" /> + <Compile Include="OAuth\ChannelElements\SigningBindingElementBaseContract.cs" /> + <Compile Include="OAuth\ChannelElements\SigningBindingElementChain.cs" /> + <Compile Include="OAuth\ChannelElements\StandardTokenGenerator.cs" /> + <Compile Include="OAuth\ChannelElements\TokenType.cs" /> + <Compile Include="OAuth\ChannelElements\UriOrOobEncoding.cs" /> + <Compile Include="OAuth\ChannelElements\TokenHandlingBindingElement.cs" /> + <Compile Include="OAuth\ConsumerBase.cs" /> + <Compile Include="OAuth\ConsumerSecuritySettings.cs" /> + <Compile Include="OAuth\DesktopConsumer.cs" /> + <Compile Include="OAuth\Messages\ITokenSecretContainingMessage.cs" /> + <Compile Include="OAuth\OAuthStrings.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>OAuthStrings.resx</DependentUpon> + </Compile> + <Compile Include="OAuth\SecuritySettings.cs" /> + <Compile Include="OAuth\ServiceProviderDescription.cs" /> + <Compile Include="OAuth\Messages\ITokenContainingMessage.cs" /> + <Compile Include="OAuth\Messages\SignedMessageBase.cs" /> + <Compile Include="OAuth\ChannelElements\SigningBindingElementBase.cs" /> + <Compile Include="OAuth\ServiceProviderSecuritySettings.cs" /> + <Compile Include="OAuth\VerificationCodeFormat.cs" /> + <Compile Include="OAuth\WebConsumer.cs" /> + <Compile Include="OAuth\ChannelElements\ITamperResistantOAuthMessage.cs" /> + <Compile Include="OAuth\Messages\MessageBase.cs" /> + <Compile Include="OAuth\Messages\AuthorizedTokenRequest.cs" /> + <Compile Include="OAuth\Messages\AccessProtectedResourceRequest.cs" /> + <Compile Include="OAuth\Messages\AuthorizedTokenResponse.cs" /> + <Compile Include="OAuth\Messages\UserAuthorizationResponse.cs" /> + <Compile Include="OAuth\Messages\UserAuthorizationRequest.cs" /> + <Compile Include="OAuth\Messages\UnauthorizedTokenResponse.cs" /> + <Compile Include="OAuth\ChannelElements\OAuthChannel.cs" /> + <Compile Include="OAuth\ChannelElements\OAuthServiceProviderMessageFactory.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="OAuth\Messages\UnauthorizedTokenRequest.cs" /> + <Compile Include="OAuth\ChannelElements\RsaSha1SigningBindingElement.cs" /> + <Compile Include="OAuth\Protocol.cs" /> + <Compile Include="OAuth\ServiceProvider.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="OAuth\ClassDiagram.cd" /> + <None Include="OAuth\Messages\OAuth Messages.cd" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="OAuth\OAuthStrings.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>OAuthStrings.Designer.cs</LastGenOutput> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="OAuth\OAuthStrings.sr.resx" /> + </ItemGroup> + <ItemGroup> + <BootstrapperPackage Include="Microsoft.Net.Client.3.5"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName> + <Install>false</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1"> + <Visible>False</Visible> + <ProductName>Windows Installer 3.1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + </ItemGroup> + <ItemGroup> + <SignDependsOn Include="BuildUnifiedProduct" /> + <DelaySignedAssemblies Include="$(ILMergeOutputAssembly);
 $(OutputPath)CodeContracts\$(ProductName).Contracts.dll;
 " /> + </ItemGroup> + <ItemGroup /> + <PropertyGroup> + <!-- Don't sign the non-unified version of the assembly. --> + <SuppressTargetPathDelaySignedAssembly>true</SuppressTargetPathDelaySignedAssembly> + </PropertyGroup> + <Target Name="BuildUnifiedProduct" DependsOnTargets="Build" Inputs="@(ILMergeInputAssemblies)" Outputs="$(ILMergeOutputAssembly)"> + <PropertyGroup> + <!-- The ILMerge task doesn't properly quote the path. --> + <ILMergeTargetPlatformDirectory Condition=" '$(ClrVersion)' == '4' ">"$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0"</ILMergeTargetPlatformDirectory> + </PropertyGroup> + <MakeDir Directories="$(ILMergeOutputAssemblyDirectory)" /> + <ILMerge ExcludeFile="$(ProjectRoot)ILMergeInternalizeExceptions.txt" InputAssemblies="@(ILMergeInputAssemblies)" OutputFile="$(ILMergeOutputAssembly)" KeyFile="$(PublicKeyFile)" DelaySign="true" ToolPath="$(ProjectRoot)tools\ILMerge" TargetPlatformVersion="$(ClrVersion).0" TargetPlatformDirectory="$(ILMergeTargetPlatformDirectory)" /> + </Target> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))\EnlistmentInfo.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))' != '' " /> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs new file mode 100644 index 0000000..5828428 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// <copyright file="HmacSha1SigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + public class HmacSha1SigningBindingElement : SigningBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="HmacSha1SigningBindingElement"/> class + /// </summary> + public HmacSha1SigningBindingElement() + : base("HMAC-SHA1") { + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message per OAuth 1.0 section 9.2. + /// </remarks> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + string key = GetConsumerAndTokenSecretString(message); + using (HashAlgorithm hasher = new HMACSHA1(Encoding.ASCII.GetBytes(key))) { + string baseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message)); + byte[] digest = hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString)); + return Convert.ToBase64String(digest); + } + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + protected override ITamperProtectionChannelBindingElement Clone() { + return new HmacSha1SigningBindingElement(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs new file mode 100644 index 0000000..dd28e71 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="ICombinedOpenIdProviderTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using DotNetOpenAuth.OpenId; + + /// <summary> + /// An interface that providers that play a dual role as OpenID Provider + /// and OAuth Service Provider should implement on their token manager classes. + /// </summary> + /// <remarks> + /// This interface should be implemented by the same class that implements + /// <see cref="ITokenManager"/> in order to enable the OpenID+OAuth extension. + /// </remarks> + public interface ICombinedOpenIdProviderTokenManager : IOpenIdOAuthTokenManager, ITokenManager { + /// <summary> + /// Gets the OAuth consumer key for a given OpenID relying party realm. + /// </summary> + /// <param name="realm">The relying party's OpenID realm.</param> + /// <returns>The OAuth consumer key for a given OpenID realm.</returns> + /// <para>This is a security-critical function. Since OpenID requests + /// and OAuth extensions for those requests can be formulated by ANYONE + /// (no signing is required by the relying party), and since the response to + /// the authentication will include access the user is granted to the + /// relying party who CLAIMS to be from some realm, it is of paramount + /// importance that the realm is recognized as belonging to the consumer + /// key by the host service provider in order to protect against phishers.</para> + string GetConsumerKey(Realm realm); + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IConsumerDescription.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IConsumerDescription.cs new file mode 100644 index 0000000..db505d5 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IConsumerDescription.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// <copyright file="IConsumerDescription.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Security.Cryptography.X509Certificates; + + /// <summary> + /// A description of a consumer from a Service Provider's point of view. + /// </summary> + public interface IConsumerDescription { + /// <summary> + /// Gets the Consumer key. + /// </summary> + string Key { get; } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + string Secret { get; } + + /// <summary> + /// Gets the certificate that can be used to verify the signature of an incoming + /// message from a Consumer. + /// </summary> + /// <returns>The public key from the Consumer's X.509 Certificate, if one can be found; otherwise <c>null</c>.</returns> + /// <remarks> + /// This property must be implemented only if the RSA-SHA1 algorithm is supported by the Service Provider. + /// </remarks> + X509Certificate2 Certificate { get; } + + /// <summary> + /// Gets the callback URI that this consumer has pre-registered with the service provider, if any. + /// </summary> + /// <value>A URI that user authorization responses should be directed to; or <c>null</c> if no preregistered callback was arranged.</value> + Uri Callback { get; } + + /// <summary> + /// Gets the verification code format that is most appropriate for this consumer + /// when a callback URI is not available. + /// </summary> + /// <value>A set of characters that can be easily keyed in by the user given the Consumer's + /// application type and form factor.</value> + /// <remarks> + /// The value <see cref="OAuth.VerificationCodeFormat.IncludedInCallback"/> should NEVER be returned + /// since this property is only used in no callback scenarios anyway. + /// </remarks> + VerificationCodeFormat VerificationCodeFormat { get; } + + /// <summary> + /// Gets the length of the verification code to issue for this Consumer. + /// </summary> + /// <value>A positive number, generally at least 4.</value> + int VerificationCodeLength { get; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IConsumerTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IConsumerTokenManager.cs new file mode 100644 index 0000000..f16be64 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IConsumerTokenManager.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------- +// <copyright file="IConsumerTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + /// <summary> + /// A token manager for use by a web site in its role as a consumer of + /// an individual ServiceProvider. + /// </summary> + public interface IConsumerTokenManager : ITokenManager { + /// <summary> + /// Gets the consumer key. + /// </summary> + /// <value>The consumer key.</value> + string ConsumerKey { get; } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + /// <value>The consumer secret.</value> + string ConsumerSecret { get; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs new file mode 100644 index 0000000..b3ee320 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdOAuthTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + + /// <summary> + /// Additional methods an <see cref="ITokenManager"/> implementing class + /// may implement to support the OpenID+OAuth extension. + /// </summary> + public interface IOpenIdOAuthTokenManager { + /// <summary> + /// Stores a new request token obtained over an OpenID request. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param> + /// <remarks> + /// <para>The token secret is the empty string.</para> + /// <para>Tokens stored by this method should be short-lived to mitigate + /// possible security threats. Their lifetime should be sufficient for the + /// relying party to receive the positive authentication assertion and immediately + /// send a follow-up request for the access token.</para> + /// </remarks> + void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization); + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderAccessToken.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderAccessToken.cs new file mode 100644 index 0000000..35ba52d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderAccessToken.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="IServiceProviderAccessToken.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Text; + + /// <summary> + /// A description of an access token and its metadata as required by a Service Provider. + /// </summary> + public interface IServiceProviderAccessToken { + /// <summary> + /// Gets the token itself. + /// </summary> + string Token { get; } + + /// <summary> + /// Gets the expiration date (local time) for the access token. + /// </summary> + /// <value>The expiration date, or <c>null</c> if there is no expiration date.</value> + DateTime? ExpirationDate { get; } + + /// <summary> + /// Gets the username of the principal that will be impersonated by this access token. + /// </summary> + /// <value> + /// The name of the user who authorized the OAuth request token originally. + /// </value> + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Username", Justification = "Breaking change.")] + string Username { get; } + + /// <summary> + /// Gets the roles that the OAuth principal should belong to. + /// </summary> + /// <value> + /// The roles that the user belongs to, or a subset of these according to the rights + /// granted when the user authorized the request token. + /// </value> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "By design.")] + string[] Roles { get; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs new file mode 100644 index 0000000..6dfa416 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// <copyright file="IServiceProviderRequestToken.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// A description of a request token and its metadata as required by a Service Provider + /// </summary> + public interface IServiceProviderRequestToken { + /// <summary> + /// Gets the token itself. + /// </summary> + string Token { get; } + + /// <summary> + /// Gets the consumer key that requested this token. + /// </summary> + string ConsumerKey { get; } + + /// <summary> + /// Gets the (local) date that this request token was first created on. + /// </summary> + DateTime CreatedOn { get; } + + /// <summary> + /// Gets or sets the callback associated specifically with this token, if any. + /// </summary> + /// <value>The callback URI; or <c>null</c> if no callback was specifically assigned to this token.</value> + Uri Callback { get; set; } + + /// <summary> + /// Gets or sets the verifier that the consumer must include in the <see cref="AuthorizedTokenRequest"/> + /// message to exchange this request token for an access token. + /// </summary> + /// <value>The verifier code, or <c>null</c> if none has been assigned (yet).</value> + string VerificationCode { get; set; } + + /// <summary> + /// Gets or sets the version of the Consumer that requested this token. + /// </summary> + /// <remarks> + /// This property is used to determine whether a <see cref="VerificationCode"/> must be + /// generated when the user authorizes the Consumer or not. + /// </remarks> + Version ConsumerVersion { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs new file mode 100644 index 0000000..7df67ce --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs @@ -0,0 +1,251 @@ +//----------------------------------------------------------------------- +// <copyright file="IServiceProviderTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// A token manager for use by a web site in its role as a + /// service provider. + /// </summary> + [ContractClass(typeof(IServiceProviderTokenManagerContract))] + public interface IServiceProviderTokenManager : ITokenManager { + /// <summary> + /// Gets the Consumer description for a given a Consumer Key. + /// </summary> + /// <param name="consumerKey">The Consumer Key.</param> + /// <returns>A description of the consumer. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the consumer key cannot be found.</exception> + IConsumerDescription GetConsumer(string consumerKey); + + /// <summary> + /// Checks whether a given request token has already been authorized + /// by some user for use by the Consumer that requested it. + /// </summary> + /// <param name="requestToken">The Consumer's request token.</param> + /// <returns> + /// True if the request token has already been fully authorized by the user + /// who owns the relevant protected resources. False if the token has not yet + /// been authorized, has expired or does not exist. + /// </returns> + bool IsRequestTokenAuthorized(string requestToken); + + /// <summary> + /// Gets details on the named request token. + /// </summary> + /// <param name="token">The request token.</param> + /// <returns>A description of the token. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the token cannot be found.</exception> + /// <remarks> + /// It is acceptable for implementations to find the token, see that it has expired, + /// delete it from the database and then throw <see cref="KeyNotFoundException"/>, + /// or alternatively it can return the expired token anyway and the OAuth channel will + /// log and throw the appropriate error. + /// </remarks> + IServiceProviderRequestToken GetRequestToken(string token); + + /// <summary> + /// Gets details on the named access token. + /// </summary> + /// <param name="token">The access token.</param> + /// <returns>A description of the token. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the token cannot be found.</exception> + /// <remarks> + /// It is acceptable for implementations to find the token, see that it has expired, + /// delete it from the database and then throw <see cref="KeyNotFoundException"/>, + /// or alternatively it can return the expired token anyway and the OAuth channel will + /// log and throw the appropriate error. + /// </remarks> + IServiceProviderAccessToken GetAccessToken(string token); + + /// <summary> + /// Persists any changes made to the token. + /// </summary> + /// <param name="token">The token whose properties have been changed.</param> + /// <remarks> + /// This library will invoke this method after making a set + /// of changes to the token as part of a web request to give the host + /// the opportunity to persist those changes to a database. + /// Depending on the object persistence framework the host site uses, + /// this method MAY not need to do anything (if changes made to the token + /// will automatically be saved without any extra handling). + /// </remarks> + void UpdateToken(IServiceProviderRequestToken token); + } + + /// <summary> + /// Code contract class for the <see cref="IServiceProviderTokenManager"/> interface. + /// </summary> + [ContractClassFor(typeof(IServiceProviderTokenManager))] + internal abstract class IServiceProviderTokenManagerContract : IServiceProviderTokenManager { + /// <summary> + /// Prevents a default instance of the <see cref="IServiceProviderTokenManagerContract"/> class from being created. + /// </summary> + private IServiceProviderTokenManagerContract() { + } + + #region IServiceProviderTokenManager Members + + /// <summary> + /// Gets the Consumer description for a given a Consumer Key. + /// </summary> + /// <param name="consumerKey">The Consumer Key.</param> + /// <returns> + /// A description of the consumer. Never null. + /// </returns> + /// <exception cref="KeyNotFoundException">Thrown if the consumer key cannot be found.</exception> + IConsumerDescription IServiceProviderTokenManager.GetConsumer(string consumerKey) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(consumerKey)); + Contract.Ensures(Contract.Result<IConsumerDescription>() != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Checks whether a given request token has already been authorized + /// by some user for use by the Consumer that requested it. + /// </summary> + /// <param name="requestToken">The Consumer's request token.</param> + /// <returns> + /// True if the request token has already been fully authorized by the user + /// who owns the relevant protected resources. False if the token has not yet + /// been authorized, has expired or does not exist. + /// </returns> + bool IServiceProviderTokenManager.IsRequestTokenAuthorized(string requestToken) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(requestToken)); + throw new NotImplementedException(); + } + + /// <summary> + /// Gets details on the named request token. + /// </summary> + /// <param name="token">The request token.</param> + /// <returns>A description of the token. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the token cannot be found.</exception> + /// <remarks> + /// It is acceptable for implementations to find the token, see that it has expired, + /// delete it from the database and then throw <see cref="KeyNotFoundException"/>, + /// or alternatively it can return the expired token anyway and the OAuth channel will + /// log and throw the appropriate error. + /// </remarks> + IServiceProviderRequestToken IServiceProviderTokenManager.GetRequestToken(string token) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(token)); + Contract.Ensures(Contract.Result<IServiceProviderRequestToken>() != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Gets details on the named access token. + /// </summary> + /// <param name="token">The access token.</param> + /// <returns>A description of the token. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the token cannot be found.</exception> + /// <remarks> + /// It is acceptable for implementations to find the token, see that it has expired, + /// delete it from the database and then throw <see cref="KeyNotFoundException"/>, + /// or alternatively it can return the expired token anyway and the OAuth channel will + /// log and throw the appropriate error. + /// </remarks> + IServiceProviderAccessToken IServiceProviderTokenManager.GetAccessToken(string token) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(token)); + Contract.Ensures(Contract.Result<IServiceProviderAccessToken>() != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Persists any changes made to the token. + /// </summary> + /// <param name="token">The token whose properties have been changed.</param> + /// <remarks> + /// This library will invoke this method after making a set + /// of changes to the token as part of a web request to give the host + /// the opportunity to persist those changes to a database. + /// Depending on the object persistence framework the host site uses, + /// this method MAY not need to do anything (if changes made to the token + /// will automatically be saved without any extra handling). + /// </remarks> + void IServiceProviderTokenManager.UpdateToken(IServiceProviderRequestToken token) { + Contract.Requires<ArgumentNullException>(token != null); + throw new NotImplementedException(); + } + + #endregion + + #region ITokenManager Members + + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns> + /// The secret associated with the given token. + /// </returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + string ITokenManager.GetTokenSecret(string token) { + throw new NotImplementedException(); + } + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ITokenManager.ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + void ITokenManager.StoreNewRequestToken(DotNetOpenAuth.OAuth.Messages.UnauthorizedTokenRequest request, DotNetOpenAuth.OAuth.Messages.ITokenSecretContainingMessage response) { + throw new NotImplementedException(); + } + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="ITokenManager.StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or + /// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + void ITokenManager.ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) { + throw new NotImplementedException(); + } + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns> + /// Request or Access token, or invalid if the token is not recognized. + /// </returns> + TokenType ITokenManager.GetTokenType(string token) { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs new file mode 100644 index 0000000..a95001d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="ITamperResistantOAuthMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An interface that OAuth messages implement to support signing. + /// </summary> + public interface ITamperResistantOAuthMessage : IDirectedProtocolMessage, ITamperResistantProtocolMessage, IMessageOriginalPayload { + /// <summary> + /// Gets or sets the method used to sign the message. + /// </summary> + string SignatureMethod { get; set; } + + /// <summary> + /// Gets or sets the Token Secret used to sign the message. + /// </summary> + string TokenSecret { get; set; } + + /// <summary> + /// Gets or sets the Consumer key. + /// </summary> + string ConsumerKey { get; set; } + + /// <summary> + /// Gets or sets the Consumer Secret used to sign the message. + /// </summary> + string ConsumerSecret { get; set; } + + /// <summary> + /// Gets or sets the HTTP method that will be used to transmit the message. + /// </summary> + string HttpMethod { get; set; } + + /// <summary> + /// Gets or sets the URL of the intended receiver of this message. + /// </summary> + new Uri Recipient { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenGenerator.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenGenerator.cs new file mode 100644 index 0000000..ce22479 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenGenerator.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenGenerator.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + /// <summary> + /// An interface allowing OAuth hosts to inject their own algorithm for generating tokens and secrets. + /// </summary> + public interface ITokenGenerator { + /// <summary> + /// Generates a new token to represent a not-yet-authorized request to access protected resources. + /// </summary> + /// <param name="consumerKey">The consumer that requested this token.</param> + /// <returns>The newly generated token.</returns> + /// <remarks> + /// This method should not store the newly generated token in any persistent store. + /// This will be done in <see cref="ITokenManager.StoreNewRequestToken"/>. + /// </remarks> + string GenerateRequestToken(string consumerKey); + + /// <summary> + /// Generates a new token to represent an authorized request to access protected resources. + /// </summary> + /// <param name="consumerKey">The consumer that requested this token.</param> + /// <returns>The newly generated token.</returns> + /// <remarks> + /// This method should not store the newly generated token in any persistent store. + /// This will be done in <see cref="ITokenManager.ExpireRequestTokenAndStoreNewAccessToken"/>. + /// </remarks> + string GenerateAccessToken(string consumerKey); + + /// <summary> + /// Returns a cryptographically strong random string for use as a token secret. + /// </summary> + /// <returns>The generated string.</returns> + string GenerateSecret(); + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs new file mode 100644 index 0000000..459cd28 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs @@ -0,0 +1,169 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// An interface OAuth hosts must implement for persistent storage + /// and recall of tokens and secrets for an individual OAuth consumer + /// or service provider. + /// </summary> + [ContractClass(typeof(ITokenManagerContract))] + public interface ITokenManager { + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns>The secret associated with the given token.</returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + string GetTokenSecret(string token); + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response); + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or + /// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret); + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns>Request or Access token, or invalid if the token is not recognized.</returns> + TokenType GetTokenType(string token); + } + + /// <summary> + /// The code contract class for the <see cref="ITokenManager"/> interface. + /// </summary> + [ContractClassFor(typeof(ITokenManager))] + internal abstract class ITokenManagerContract : ITokenManager { + /// <summary> + /// Prevents a default instance of the <see cref="ITokenManagerContract"/> class from being created. + /// </summary> + private ITokenManagerContract() { + } + + #region ITokenManager Members + + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns> + /// The secret associated with the given token. + /// </returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + string ITokenManager.GetTokenSecret(string token) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(token)); + Contract.Ensures(Contract.Result<string>() != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ITokenManager.ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + void ITokenManager.StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentNullException>(response != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="ITokenManager.StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or + /// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + void ITokenManager.ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(consumerKey)); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(requestToken)); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(accessToken)); + Contract.Requires<ArgumentNullException>(accessTokenSecret != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns> + /// Request or Access token, or invalid if the token is not recognized. + /// </returns> + TokenType ITokenManager.GetTokenType(string token) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(token)); + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs new file mode 100644 index 0000000..76b5149 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -0,0 +1,410 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthChannel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// An OAuth-specific implementation of the <see cref="Channel"/> class. + /// </summary> + internal class OAuthChannel : Channel { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthChannel"/> class. + /// </summary> + /// <param name="signingBindingElement">The binding element to use for signing.</param> + /// <param name="store">The web application store to use for nonces.</param> + /// <param name="tokenManager">The token manager instance to use.</param> + /// <param name="securitySettings">The security settings.</param> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")] + internal OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, IConsumerTokenManager tokenManager, ConsumerSecuritySettings securitySettings) + : this( + signingBindingElement, + store, + tokenManager, + securitySettings, + new OAuthConsumerMessageFactory()) { + Contract.Requires<ArgumentNullException>(tokenManager != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + Contract.Requires<ArgumentNullException>(signingBindingElement != null); + Contract.Requires<ArgumentException>(signingBindingElement.SignatureCallback == null, OAuthStrings.SigningElementAlreadyAssociatedWithChannel); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthChannel"/> class. + /// </summary> + /// <param name="signingBindingElement">The binding element to use for signing.</param> + /// <param name="store">The web application store to use for nonces.</param> + /// <param name="tokenManager">The token manager instance to use.</param> + /// <param name="securitySettings">The security settings.</param> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")] + internal OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, IServiceProviderTokenManager tokenManager, ServiceProviderSecuritySettings securitySettings) + : this( + signingBindingElement, + store, + tokenManager, + securitySettings, + new OAuthServiceProviderMessageFactory(tokenManager)) { + Contract.Requires<ArgumentNullException>(tokenManager != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + Contract.Requires<ArgumentNullException>(signingBindingElement != null); + Contract.Requires<ArgumentException>(signingBindingElement.SignatureCallback == null, OAuthStrings.SigningElementAlreadyAssociatedWithChannel); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthChannel"/> class. + /// </summary> + /// <param name="signingBindingElement">The binding element to use for signing.</param> + /// <param name="store">The web application store to use for nonces.</param> + /// <param name="tokenManager">The ITokenManager instance to use.</param> + /// <param name="securitySettings">The security settings.</param> + /// <param name="messageTypeProvider">An injected message type provider instance. + /// Except for mock testing, this should always be one of + /// <see cref="OAuthConsumerMessageFactory"/> or <see cref="OAuthServiceProviderMessageFactory"/>.</param> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")] + internal OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, SecuritySettings securitySettings, IMessageFactory messageTypeProvider) + : base(messageTypeProvider, InitializeBindingElements(signingBindingElement, store, tokenManager, securitySettings)) { + Contract.Requires<ArgumentNullException>(tokenManager != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + Contract.Requires<ArgumentNullException>(signingBindingElement != null); + Contract.Requires<ArgumentException>(signingBindingElement.SignatureCallback == null, OAuthStrings.SigningElementAlreadyAssociatedWithChannel); + + this.TokenManager = tokenManager; + signingBindingElement.SignatureCallback = this.SignatureCallback; + } + + /// <summary> + /// Gets or sets the Consumer web application path. + /// </summary> + internal Uri Realm { get; set; } + + /// <summary> + /// Gets the token manager being used. + /// </summary> + protected internal ITokenManager TokenManager { get; private set; } + + /// <summary> + /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1. + /// </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(IEnumerable<KeyValuePair<string, string>> message) { + var encodedDictionary = new Dictionary<string, string>(); + UriEscapeParameters(message, encodedDictionary); + return encodedDictionary; + } + + /// <summary> + /// Initializes a web request for sending by attaching a message to it. + /// Use this method to prepare a protected resource request that you do NOT + /// expect an OAuth message response to. + /// </summary> + /// <param name="request">The message to attach.</param> + /// <returns>The initialized web request.</returns> + internal HttpWebRequest InitializeRequest(IDirectedProtocolMessage request) { + Contract.Requires<ArgumentNullException>(request != null); + + ProcessOutgoingMessage(request); + return this.CreateHttpRequest(request); + } + + /// <summary> + /// Searches an incoming HTTP request for data that could be used to assemble + /// a protocol request message. + /// </summary> + /// <param name="request">The HTTP request to search.</param> + /// <returns>The deserialized message, if one is found. Null otherwise.</returns> + protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) { + // First search the Authorization header. + string authorization = request.Headers[HttpRequestHeader.Authorization]; + var fields = MessagingUtilities.ParseAuthorizationHeader(Protocol.AuthorizationHeaderScheme, authorization).ToDictionary(); + fields.Remove("realm"); // ignore the realm parameter, since we don't use it, and it must be omitted from signature base string. + + // Scrape the entity + if (!string.IsNullOrEmpty(request.Headers[HttpRequestHeader.ContentType])) { + var contentType = new ContentType(request.Headers[HttpRequestHeader.ContentType]); + if (string.Equals(contentType.MediaType, HttpFormUrlEncoded, StringComparison.Ordinal)) { + foreach (string key in request.Form) { + if (key != null) { + fields.Add(key, request.Form[key]); + } else { + Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.Form[key]); + } + } + } + } + + // Scrape the query string + foreach (string key in request.QueryStringBeforeRewriting) { + if (key != null) { + fields.Add(key, request.QueryStringBeforeRewriting[key]); + } else { + Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.QueryStringBeforeRewriting[key]); + } + } + + MessageReceivingEndpoint recipient; + try { + recipient = request.GetRecipient(); + } catch (ArgumentException ex) { + Logger.OAuth.WarnFormat("Unrecognized HTTP request: " + ex.ToString()); + return null; + } + + // Deserialize the message using all the data we've collected. + var message = (IDirectedProtocolMessage)this.Receive(fields, recipient); + + // Add receiving HTTP transport information required for signature generation. + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null) { + signedMessage.Recipient = request.UrlBeforeRewriting; + signedMessage.HttpMethod = request.HttpMethod; + } + + return message; + } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + string body = response.GetResponseReader().ReadToEnd(); + return HttpUtility.ParseQueryString(body).ToDictionary(); + } + + /// <summary> + /// Prepares an HTTP request that carries a given message. + /// </summary> + /// <param name="request">The message to send.</param> + /// <returns> + /// The <see cref="HttpRequest"/> prepared to send the request. + /// </returns> + protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { + HttpWebRequest httpRequest; + + HttpDeliveryMethods transmissionMethod = request.HttpMethods; + 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); + } else if ((transmissionMethod & HttpDeliveryMethods.HeadRequest) != 0) { + httpRequest = InitializeRequestAsHead(request); + } else if ((transmissionMethod & HttpDeliveryMethods.PutRequest) != 0) { + httpRequest = this.InitializeRequestAsPut(request); + } else if ((transmissionMethod & HttpDeliveryMethods.DeleteRequest) != 0) { + httpRequest = InitializeRequestAsDelete(request); + } else { + throw new NotSupportedException(); + } + return httpRequest; + } + + /// <summary> + /// Queues a message for sending in the response stream where the fields + /// are sent in the response stream in querystring style. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> + /// <remarks> + /// This method implements spec V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + var messageAccessor = this.MessageDescriptions.GetAccessor(response); + var fields = messageAccessor.Serialize(); + string responseBody = MessagingUtilities.CreateQueryString(fields); + + OutgoingWebResponse encodedResponse = new OutgoingWebResponse { + Body = responseBody, + OriginalMessage = response, + Status = HttpStatusCode.OK, + Headers = new System.Net.WebHeaderCollection(), + }; + + IHttpDirectResponse httpMessage = response as IHttpDirectResponse; + if (httpMessage != null) { + encodedResponse.Status = httpMessage.HttpStatusCode; + } + + return encodedResponse; + } + + /// <summary> + /// Initializes the binding elements for the OAuth channel. + /// </summary> + /// <param name="signingBindingElement">The signing binding element.</param> + /// <param name="store">The nonce store.</param> + /// <param name="tokenManager">The token manager.</param> + /// <param name="securitySettings">The security settings.</param> + /// <returns> + /// An array of binding elements used to initialize the channel. + /// </returns> + private static IChannelBindingElement[] InitializeBindingElements(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, SecuritySettings securitySettings) { + Contract.Requires(securitySettings != null); + + var bindingElements = new List<IChannelBindingElement> { + new OAuthHttpMethodBindingElement(), + signingBindingElement, + new StandardExpirationBindingElement(), + new StandardReplayProtectionBindingElement(store), + }; + + var spTokenManager = tokenManager as IServiceProviderTokenManager; + var serviceProviderSecuritySettings = securitySettings as ServiceProviderSecuritySettings; + if (spTokenManager != null && serviceProviderSecuritySettings != null) { + bindingElements.Insert(0, new TokenHandlingBindingElement(spTokenManager, serviceProviderSecuritySettings)); + } + + return bindingElements.ToArray(); + } + + /// <summary> + /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1. + /// </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(IEnumerable<KeyValuePair<string, string>> source, IDictionary<string, string> destination) { + Contract.Requires<ArgumentNullException>(source != null); + Contract.Requires<ArgumentNullException>(destination != null); + + foreach (var pair in source) { + var key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key); + var value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value); + destination.Add(key, value); + } + } + + /// <summary> + /// Gets the HTTP method to use for a message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>"POST", "GET" or some other similar http verb.</returns> + private static string GetHttpMethod(IDirectedProtocolMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null) { + return signedMessage.HttpMethod; + } else { + return MessagingUtilities.GetHttpVerb(message.HttpMethods); + } + } + + /// <summary> + /// Prepares to send a request to the Service Provider via the Authorization header. + /// </summary> + /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param> + /// <returns>The web request ready to send.</returns> + /// <remarks> + /// <para>If the message has non-empty ExtraData in it, the request stream is sent to + /// the server automatically. If it is empty, the request stream must be sent by the caller.</para> + /// <para>This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4).</para> + /// </remarks> + private HttpWebRequest InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) { + var dictionary = this.MessageDescriptions.GetAccessor(requestMessage); + + // copy so as to not modify original + var fields = new Dictionary<string, string>(); + foreach (string key in dictionary.DeclaredKeys) { + fields.Add(key, dictionary[key]); + } + if (this.Realm != null) { + fields.Add("realm", this.Realm.AbsoluteUri); + } + + HttpWebRequest httpRequest; + UriBuilder recipientBuilder = new UriBuilder(requestMessage.Recipient); + bool hasEntity = HttpMethodHasEntity(GetHttpMethod(requestMessage)); + + if (!hasEntity) { + MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData); + } + httpRequest = (HttpWebRequest)WebRequest.Create(recipientBuilder.Uri); + httpRequest.Method = GetHttpMethod(requestMessage); + + httpRequest.Headers.Add(HttpRequestHeader.Authorization, MessagingUtilities.AssembleAuthorizationHeader(Protocol.AuthorizationHeaderScheme, fields)); + + 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. + 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 { + 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; + } + } + } + + return httpRequest; + } + + /// <summary> + /// Fills out the secrets in a message so that signing/verification can be performed. + /// </summary> + /// <param name="message">The message about to be signed or whose signature is about to be verified.</param> + private void SignatureCallback(ITamperResistantProtocolMessage message) { + var oauthMessage = message as ITamperResistantOAuthMessage; + try { + Logger.Channel.Debug("Applying secrets to message to prepare for signing or signature verification."); + oauthMessage.ConsumerSecret = this.GetConsumerSecret(oauthMessage.ConsumerKey); + + var tokenMessage = message as ITokenContainingMessage; + if (tokenMessage != null) { + oauthMessage.TokenSecret = this.TokenManager.GetTokenSecret(tokenMessage.Token); + } + } catch (KeyNotFoundException ex) { + throw new ProtocolException(OAuthStrings.ConsumerOrTokenSecretNotFound, ex); + } + } + + /// <summary> + /// Gets the consumer secret for a given consumer key. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <returns>The consumer secret.</returns> + private string GetConsumerSecret(string consumerKey) { + var consumerTokenManager = this.TokenManager as IConsumerTokenManager; + if (consumerTokenManager != null) { + ErrorUtilities.VerifyInternal(consumerKey == consumerTokenManager.ConsumerKey, "The token manager consumer key and the consumer key set earlier do not match!"); + return consumerTokenManager.ConsumerSecret; + } else { + return ((IServiceProviderTokenManager)this.TokenManager).GetConsumer(consumerKey).Secret; + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs new file mode 100644 index 0000000..327b923 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthConsumerMessageFactory.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// An OAuth-protocol specific implementation of the <see cref="IMessageFactory"/> + /// interface. + /// </summary> + public class OAuthConsumerMessageFactory : IMessageFactory { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthConsumerMessageFactory"/> class. + /// </summary> + protected internal OAuthConsumerMessageFactory() { + } + + #region IMessageFactory Members + + /// <summary> + /// Analyzes an incoming request message payload to discover what kind of + /// message is embedded in it and returns the type, or null if no match is found. + /// </summary> + /// <param name="recipient">The intended or actual recipient of the request message.</param> + /// <param name="fields">The name/value pairs that make up the message payload.</param> + /// <returns> + /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can + /// deserialize to. Null if the request isn't recognized as a valid protocol message. + /// </returns> + /// <remarks> + /// The request messages are: + /// UserAuthorizationResponse + /// </remarks> + public virtual IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) { + MessageBase message = null; + + if (fields.ContainsKey("oauth_token")) { + Protocol protocol = fields.ContainsKey("oauth_verifier") ? Protocol.V10a : Protocol.V10; + message = new UserAuthorizationResponse(recipient.Location, protocol.Version); + } + + if (message != null) { + message.SetAsIncoming(); + } + + return message; + } + + /// <summary> + /// Analyzes an incoming request message payload to discover what kind of + /// message is embedded in it and returns the type, or null if no match is found. + /// </summary> + /// <param name="request"> + /// The message that was sent as a request that resulted in the response. + /// Null on a Consumer site that is receiving an indirect message from the Service Provider. + /// </param> + /// <param name="fields">The name/value pairs that make up the message payload.</param> + /// <returns> + /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can + /// deserialize to. Null if the request isn't recognized as a valid protocol message. + /// </returns> + /// <remarks> + /// The response messages are: + /// UnauthorizedTokenResponse + /// AuthorizedTokenResponse + /// </remarks> + public virtual IDirectResponseProtocolMessage GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields) { + MessageBase message = null; + + // All response messages have the oauth_token field. + if (!fields.ContainsKey("oauth_token")) { + return null; + } + + // All direct message responses should have the oauth_token_secret field. + if (!fields.ContainsKey("oauth_token_secret")) { + Logger.OAuth.Error("An OAuth message was expected to contain an oauth_token_secret but didn't."); + return null; + } + + var unauthorizedTokenRequest = request as UnauthorizedTokenRequest; + var authorizedTokenRequest = request as AuthorizedTokenRequest; + if (unauthorizedTokenRequest != null) { + Protocol protocol = fields.ContainsKey("oauth_callback_confirmed") ? Protocol.V10a : Protocol.V10; + message = new UnauthorizedTokenResponse(unauthorizedTokenRequest, protocol.Version); + } else if (authorizedTokenRequest != null) { + message = new AuthorizedTokenResponse(authorizedTokenRequest); + } else { + Logger.OAuth.ErrorFormat("Unexpected response message given the request type {0}", request.GetType().Name); + throw new ProtocolException(OAuthStrings.InvalidIncomingMessage); + } + + if (message != null) { + message.SetAsIncoming(); + } + + return message; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs new file mode 100644 index 0000000..37fb80b --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthHttpMethodBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Sets the HTTP Method property on a signed message before the signing module gets to it. + /// </summary> + internal class OAuthHttpMethodBindingElement : IChannelBindingElement { + #region IChannelBindingElement Members + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// True if the <paramref name="message"/> applied to this binding element + /// and the operation was successful. False otherwise. + /// </returns> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var oauthMessage = message as ITamperResistantOAuthMessage; + + if (oauthMessage != null) { + HttpDeliveryMethods transmissionMethod = oauthMessage.HttpMethods; + try { + oauthMessage.HttpMethod = MessagingUtilities.GetHttpVerb(transmissionMethod); + return MessageProtections.None; + } catch (ArgumentException ex) { + Logger.OAuth.Error("Unrecognized HttpDeliveryMethods value.", ex); + return null; + } + } else { + return null; + } + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// True if the <paramref name="message"/> applied to this binding element + /// and the operation was successful. False if the operation did not apply to this message. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthIdentity.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthIdentity.cs new file mode 100644 index 0000000..65bde20 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthIdentity.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthIdentity.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Runtime.InteropServices; + using System.Security.Principal; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Represents an OAuth consumer that is impersonating a known user on the system. + /// </summary> + [SuppressMessage("Microsoft.Interoperability", "CA1409:ComVisibleTypesShouldBeCreatable", Justification = "Not cocreatable.")] + [Serializable] + [ComVisible(true)] + public class OAuthIdentity : IIdentity { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthIdentity"/> class. + /// </summary> + /// <param name="username">The username.</param> + internal OAuthIdentity(string username) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(username)); + this.Name = username; + } + + #region IIdentity Members + + /// <summary> + /// Gets the type of authentication used. + /// </summary> + /// <value>The constant "OAuth"</value> + /// <returns> + /// The type of authentication used to identify the user. + /// </returns> + public string AuthenticationType { + get { return "OAuth"; } + } + + /// <summary> + /// Gets a value indicating whether the user has been authenticated. + /// </summary> + /// <value>The value <c>true</c></value> + /// <returns>true if the user was authenticated; otherwise, false. + /// </returns> + public bool IsAuthenticated { + get { return true; } + } + + /// <summary> + /// Gets the name of the user who authorized the OAuth token the consumer is using for authorization. + /// </summary> + /// <returns> + /// The name of the user on whose behalf the code is running. + /// </returns> + public string Name { get; private set; } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthPrincipal.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthPrincipal.cs new file mode 100644 index 0000000..82ecb0a --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthPrincipal.cs @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthPrincipal.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Runtime.InteropServices; + using System.Security.Principal; + + /// <summary> + /// Represents an OAuth consumer that is impersonating a known user on the system. + /// </summary> + [SuppressMessage("Microsoft.Interoperability", "CA1409:ComVisibleTypesShouldBeCreatable", Justification = "Not cocreatable.")] + [Serializable] + [ComVisible(true)] + public class OAuthPrincipal : IPrincipal { + /// <summary> + /// The roles this user belongs to. + /// </summary> + private ICollection<string> roles; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthPrincipal"/> class. + /// </summary> + /// <param name="userName">The username.</param> + /// <param name="roles">The roles this user belongs to.</param> + public OAuthPrincipal(string userName, string[] roles) + : this(new OAuthIdentity(userName), roles) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthPrincipal"/> class. + /// </summary> + /// <param name="token">The access token.</param> + internal OAuthPrincipal(IServiceProviderAccessToken token) + : this(token.Username, token.Roles) { + Contract.Requires<ArgumentNullException>(token != null); + + this.AccessToken = token.Token; + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthPrincipal"/> class. + /// </summary> + /// <param name="identity">The identity.</param> + /// <param name="roles">The roles this user belongs to.</param> + internal OAuthPrincipal(OAuthIdentity identity, string[] roles) { + this.Identity = identity; + this.roles = roles; + } + + /// <summary> + /// Gets the access token used to create this principal. + /// </summary> + /// <value>A non-empty string.</value> + public string AccessToken { get; private set; } + + #region IPrincipal Members + + /// <summary> + /// Gets the identity of the current principal. + /// </summary> + /// <value></value> + /// <returns> + /// The <see cref="T:System.Security.Principal.IIdentity"/> object associated with the current principal. + /// </returns> + public IIdentity Identity { get; private set; } + + /// <summary> + /// Determines whether the current principal belongs to the specified role. + /// </summary> + /// <param name="role">The name of the role for which to check membership.</param> + /// <returns> + /// true if the current principal is a member of the specified role; otherwise, false. + /// </returns> + /// <remarks> + /// The role membership check uses <see cref="StringComparer.OrdinalIgnoreCase"/>. + /// </remarks> + public bool IsInRole(string role) { + return this.roles.Contains(role, StringComparer.OrdinalIgnoreCase); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs new file mode 100644 index 0000000..5b3c918 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthServiceProviderMessageFactory.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// An OAuth-protocol specific implementation of the <see cref="IMessageFactory"/> + /// interface. + /// </summary> + public class OAuthServiceProviderMessageFactory : IMessageFactory { + /// <summary> + /// The token manager to use for discerning between request and access tokens. + /// </summary> + private IServiceProviderTokenManager tokenManager; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthServiceProviderMessageFactory"/> class. + /// </summary> + /// <param name="tokenManager">The token manager instance to use.</param> + public OAuthServiceProviderMessageFactory(IServiceProviderTokenManager tokenManager) { + Contract.Requires<ArgumentNullException>(tokenManager != null); + + this.tokenManager = tokenManager; + } + + #region IMessageFactory Members + + /// <summary> + /// Analyzes an incoming request message payload to discover what kind of + /// message is embedded in it and returns the type, or null if no match is found. + /// </summary> + /// <param name="recipient">The intended or actual recipient of the request message.</param> + /// <param name="fields">The name/value pairs that make up the message payload.</param> + /// <returns> + /// A newly instantiated <see cref="IProtocolMessage"/>-derived object that this message can + /// deserialize to. Null if the request isn't recognized as a valid protocol message. + /// </returns> + /// <remarks> + /// The request messages are: + /// UnauthorizedTokenRequest + /// AuthorizedTokenRequest + /// UserAuthorizationRequest + /// AccessProtectedResourceRequest + /// </remarks> + public virtual IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> fields) { + MessageBase message = null; + Protocol protocol = Protocol.V10; // default to assuming the less-secure 1.0 instead of 1.0a until we prove otherwise. + string token; + fields.TryGetValue("oauth_token", out token); + + try { + if (fields.ContainsKey("oauth_consumer_key") && !fields.ContainsKey("oauth_token")) { + protocol = fields.ContainsKey("oauth_callback") ? Protocol.V10a : Protocol.V10; + message = new UnauthorizedTokenRequest(recipient, protocol.Version); + } else if (fields.ContainsKey("oauth_consumer_key") && fields.ContainsKey("oauth_token")) { + // Discern between RequestAccessToken and AccessProtectedResources, + // which have all the same parameters, by figuring out what type of token + // is in the token parameter. + bool tokenTypeIsAccessToken = this.tokenManager.GetTokenType(token) == TokenType.AccessToken; + + if (tokenTypeIsAccessToken) { + message = (MessageBase)new AccessProtectedResourceRequest(recipient, protocol.Version); + } else { + // Discern between 1.0 and 1.0a requests by checking on the consumer version we stored + // when the consumer first requested an unauthorized token. + protocol = Protocol.Lookup(this.tokenManager.GetRequestToken(token).ConsumerVersion); + message = new AuthorizedTokenRequest(recipient, protocol.Version); + } + } else { + // fail over to the message with no required fields at all. + if (token != null) { + protocol = Protocol.Lookup(this.tokenManager.GetRequestToken(token).ConsumerVersion); + } + + // If a callback parameter is included, that suggests either the consumer + // is following OAuth 1.0 instead of 1.0a, or that a hijacker is trying + // to attack. Either way, if the consumer started out as a 1.0a, keep it + // that way, and we'll just ignore the oauth_callback included in this message + // by virtue of the UserAuthorizationRequest message not including it in its + // 1.0a payload. + message = new UserAuthorizationRequest(recipient, protocol.Version); + } + + if (message != null) { + message.SetAsIncoming(); + } + + return message; + } catch (KeyNotFoundException ex) { + throw ErrorUtilities.Wrap(ex, OAuthStrings.TokenNotFound); + } + } + + /// <summary> + /// Analyzes an incoming request message payload to discover what kind of + /// message is embedded in it and returns the type, or null if no match is found. + /// </summary> + /// <param name="request"> + /// The message that was sent as a request that resulted in the response. + /// Null on a Consumer site that is receiving an indirect message from the Service Provider. + /// </param> + /// <param name="fields">The name/value pairs that make up the message payload.</param> + /// <returns> + /// The <see cref="IProtocolMessage"/>-derived concrete class that this message can + /// deserialize to. Null if the request isn't recognized as a valid protocol message. + /// </returns> + /// <remarks> + /// The response messages are: + /// None. + /// </remarks> + public virtual IDirectResponseProtocolMessage GetNewResponseMessage(IDirectedProtocolMessage request, IDictionary<string, string> fields) { + Logger.OAuth.Error("Service Providers are not expected to ever receive responses."); + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs new file mode 100644 index 0000000..22e5f20 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="PlaintextSigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + public class PlaintextSigningBindingElement : SigningBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="PlaintextSigningBindingElement"/> class. + /// </summary> + public PlaintextSigningBindingElement() + : base("PLAINTEXT") { + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message according to OAuth 1.0 section 9.4.1. + /// </remarks> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + return GetConsumerAndTokenSecretString(message); + } + + /// <summary> + /// Checks whether this binding element applies to this message. + /// </summary> + /// <param name="message">The message that needs to be signed.</param> + /// <returns>True if this binding element can be used to sign the message. False otherwise.</returns> + protected override bool IsMessageApplicable(ITamperResistantOAuthMessage message) { + if (string.Equals(message.Recipient.Scheme, "https", StringComparison.OrdinalIgnoreCase)) { + return true; + } else { + Logger.Bindings.DebugFormat("The {0} element will not sign this message because the URI scheme is not https.", this.GetType().Name); + return false; + } + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + protected override ITamperProtectionChannelBindingElement Clone() { + return new PlaintextSigningBindingElement(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs new file mode 100644 index 0000000..f7b8370 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs @@ -0,0 +1,115 @@ +//----------------------------------------------------------------------- +// <copyright file="RsaSha1SigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + public class RsaSha1SigningBindingElement : SigningBindingElementBase { + /// <summary> + /// The name of the hash algorithm to use. + /// </summary> + private const string HashAlgorithmName = "RSA-SHA1"; + + /// <summary> + /// The token manager for the service provider. + /// </summary> + private IServiceProviderTokenManager tokenManager; + + /// <summary> + /// Initializes a new instance of the <see cref="RsaSha1SigningBindingElement"/> class + /// for use by Consumers. + /// </summary> + /// <param name="signingCertificate">The certificate used to sign outgoing messages.</param> + public RsaSha1SigningBindingElement(X509Certificate2 signingCertificate) + : base(HashAlgorithmName) { + Contract.Requires<ArgumentNullException>(signingCertificate != null); + + this.SigningCertificate = signingCertificate; + } + + /// <summary> + /// Initializes a new instance of the <see cref="RsaSha1SigningBindingElement"/> class + /// for use by Service Providers. + /// </summary> + /// <param name="tokenManager">The token manager.</param> + public RsaSha1SigningBindingElement(IServiceProviderTokenManager tokenManager) + : base(HashAlgorithmName) { + Contract.Requires<ArgumentNullException>(tokenManager != null); + + this.tokenManager = tokenManager; + } + + /// <summary> + /// Gets or sets the certificate used to sign outgoing messages. Used only by Consumers. + /// </summary> + public X509Certificate2 SigningCertificate { get; set; } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message per OAuth 1.0 section 9.3. + /// </remarks> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + ErrorUtilities.VerifyOperation(this.SigningCertificate != null, OAuthStrings.X509CertificateNotProvidedForSigning); + + string signatureBaseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message)); + byte[] data = Encoding.ASCII.GetBytes(signatureBaseString); + var provider = (RSACryptoServiceProvider)this.SigningCertificate.PrivateKey; + byte[] binarySignature = provider.SignData(data, "SHA1"); + string base64Signature = Convert.ToBase64String(binarySignature); + return base64Signature; + } + + /// <summary> + /// Determines whether the signature on some message is valid. + /// </summary> + /// <param name="message">The message to check the signature on.</param> + /// <returns> + /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>. + /// </returns> + protected override bool IsSignatureValid(ITamperResistantOAuthMessage message) { + ErrorUtilities.VerifyInternal(this.tokenManager != null, "No token manager available for fetching Consumer public certificates."); + + string signatureBaseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message)); + byte[] data = Encoding.ASCII.GetBytes(signatureBaseString); + + byte[] carriedSignature = Convert.FromBase64String(message.Signature); + + X509Certificate2 cert = this.tokenManager.GetConsumer(message.ConsumerKey).Certificate; + if (cert == null) { + Logger.Signatures.WarnFormat("Incoming message from consumer '{0}' could not be matched with an appropriate X.509 certificate for signature verification.", message.ConsumerKey); + return false; + } + + var provider = (RSACryptoServiceProvider)cert.PublicKey.Key; + bool valid = provider.VerifyData(data, "SHA1", carriedSignature); + return valid; + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + protected override ITamperProtectionChannelBindingElement Clone() { + if (this.tokenManager != null) { + return new RsaSha1SigningBindingElement(this.tokenManager); + } else { + return new RsaSha1SigningBindingElement(this.SigningCertificate); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs new file mode 100644 index 0000000..31b5149 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -0,0 +1,329 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + [ContractClass(typeof(SigningBindingElementBaseContract))] + public abstract class SigningBindingElementBase : ITamperProtectionChannelBindingElement { + /// <summary> + /// The signature method this binding element uses. + /// </summary> + private string signatureMethod; + + /// <summary> + /// Initializes a new instance of the <see cref="SigningBindingElementBase"/> class. + /// </summary> + /// <param name="signatureMethod">The OAuth signature method that the binding element uses.</param> + internal SigningBindingElementBase(string signatureMethod) { + this.signatureMethod = signatureMethod; + } + + #region IChannelBindingElement Properties + + /// <summary> + /// Gets the message protection provided by this binding element. + /// </summary> + public MessageProtections Protection { + get { return MessageProtections.TamperProtection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + #endregion + + #region ITamperProtectionChannelBindingElement members + + /// <summary> + /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a signed + /// message so that its signature can be correctly calculated for verification. + /// </summary> + public Action<ITamperResistantOAuthMessage> SignatureCallback { get; set; } + + /// <summary> + /// Creates a new object that is a copy of the current instance. + /// </summary> + /// <returns> + /// A new object that is a copy of this instance. + /// </returns> + ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() { + ITamperProtectionChannelBindingElement clone = this.Clone(); + clone.SignatureCallback = this.SignatureCallback; + return clone; + } + + #endregion + + #region IChannelBindingElement Methods + + /// <summary> + /// Signs the outgoing message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null && this.IsMessageApplicable(signedMessage)) { + if (this.SignatureCallback != null) { + this.SignatureCallback(signedMessage); + } else { + Logger.Bindings.Warn("Signing required, but callback delegate was not provided to provide additional data for signing."); + } + + signedMessage.SignatureMethod = this.signatureMethod; + Logger.Bindings.DebugFormat("Signing {0} message using {1}.", message.GetType().Name, this.signatureMethod); + signedMessage.Signature = this.GetSignature(signedMessage); + return MessageProtections.TamperProtection; + } + + return null; + } + + /// <summary> + /// Verifies the signature on an incoming message. + /// </summary> + /// <param name="message">The message whose signature should be verified.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="InvalidSignatureException">Thrown if the signature is invalid.</exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null && this.IsMessageApplicable(signedMessage)) { + Logger.Bindings.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature); + + if (!string.Equals(signedMessage.SignatureMethod, this.signatureMethod, StringComparison.Ordinal)) { + Logger.Bindings.WarnFormat("Expected signature method '{0}' but received message with a signature method of '{1}'.", this.signatureMethod, signedMessage.SignatureMethod); + return MessageProtections.None; + } + + if (this.SignatureCallback != null) { + this.SignatureCallback(signedMessage); + } else { + Logger.Bindings.Warn("Signature verification required, but callback delegate was not provided to provide additional data for signature verification."); + } + + if (!this.IsSignatureValid(signedMessage)) { + Logger.Bindings.Error("Signature verification failed."); + throw new InvalidSignatureException(message); + } + + return MessageProtections.TamperProtection; + } + + return null; + } + + #endregion + + /// <summary> + /// Constructs the OAuth Signature Base String and returns the result. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="messageDictionary">The message to derive the signature base string from.</param> + /// <returns>The signature base string.</returns> + /// <remarks> + /// This method implements OAuth 1.0 section 9.1. + /// </remarks> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")] + internal static string ConstructSignatureBaseString(ITamperResistantOAuthMessage message, MessageDictionary messageDictionary) { + Contract.Requires<ArgumentNullException>(message != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(message.HttpMethod)); + Contract.Requires<ArgumentNullException>(messageDictionary != null); + Contract.Requires<ArgumentException>(messageDictionary.Message == message); + + List<string> signatureBaseStringElements = new List<string>(3); + + signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant()); + + // 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; + } + + // If this message was deserialized, include only those explicitly included message parts (excludes defaulted values) + // in the signature. + var originalPayloadMessage = (IMessageOriginalPayload)message; + if (originalPayloadMessage.OriginalPayload != null) { + partsToInclude = partsToInclude.Where(pair => originalPayloadMessage.OriginalPayload.ContainsKey(pair.Key)); + } + + 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 + // in the query that are not in the message dictionary because they were included + // in the receiving endpoint (the original URL). + // In an outgoing message, the POST entity can only contain parameters if they were + // in the message dictionary, so no need to pull out any parameters from there. + if (message.Recipient.Query != null) { + NameValueCollection nvc = HttpUtility.ParseQueryString(message.Recipient.Query); + foreach (string key in nvc) { + string escapedKey = MessagingUtilities.EscapeUriDataStringRfc3986(key); + string escapedValue = MessagingUtilities.EscapeUriDataStringRfc3986(nvc[key]); + string existingValue; + if (!encodedDictionary.TryGetValue(escapedKey, out existingValue)) { + encodedDictionary.Add(escapedKey, escapedValue); + } else { + ErrorUtilities.VerifyInternal(escapedValue == existingValue, "Somehow we have conflicting values for the '{0}' parameter.", escapedKey); + } + } + } + encodedDictionary.Remove("oauth_signature"); + + UriBuilder endpoint = new UriBuilder(message.Recipient); + endpoint.Query = null; + endpoint.Fragment = null; + signatureBaseStringElements.Add(endpoint.Uri.AbsoluteUri); + + var sortedKeyValueList = new List<KeyValuePair<string, string>>(encodedDictionary); + sortedKeyValueList.Sort(SignatureBaseStringParameterComparer); + StringBuilder paramBuilder = new StringBuilder(); + foreach (var pair in sortedKeyValueList) { + if (paramBuilder.Length > 0) { + paramBuilder.Append("&"); + } + + paramBuilder.Append(pair.Key); + paramBuilder.Append('='); + paramBuilder.Append(pair.Value); + } + + signatureBaseStringElements.Add(paramBuilder.ToString()); + + StringBuilder signatureBaseString = new StringBuilder(); + foreach (string element in signatureBaseStringElements) { + if (signatureBaseString.Length > 0) { + signatureBaseString.Append("&"); + } + + signatureBaseString.Append(MessagingUtilities.EscapeUriDataStringRfc3986(element)); + } + + Logger.Bindings.DebugFormat("Constructed signature base string: {0}", signatureBaseString); + return signatureBaseString.ToString(); + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message per OAuth 1.0 section 9.2. + /// </remarks> + internal string GetSignatureTestHook(ITamperResistantOAuthMessage message) { + return this.GetSignature(message); + } + + /// <summary> + /// Gets the "ConsumerSecret&TokenSecret" string, allowing either property to be empty or null. + /// </summary> + /// <param name="message">The message to extract the secrets from.</param> + /// <returns>The concatenated string.</returns> + protected static string GetConsumerAndTokenSecretString(ITamperResistantOAuthMessage message) { + StringBuilder builder = new StringBuilder(); + if (!string.IsNullOrEmpty(message.ConsumerSecret)) { + builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.ConsumerSecret)); + } + builder.Append("&"); + if (!string.IsNullOrEmpty(message.TokenSecret)) { + builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.TokenSecret)); + } + return builder.ToString(); + } + + /// <summary> + /// Determines whether the signature on some message is valid. + /// </summary> + /// <param name="message">The message to check the signature on.</param> + /// <returns> + /// <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 MessagingUtilities.EqualsConstantTime(message.Signature, signature); + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + /// <remarks> + /// Implementations of this method need not clone the SignatureVerificationCallback member, as the + /// <see cref="SigningBindingElementBase"/> class does this. + /// </remarks> + protected abstract ITamperProtectionChannelBindingElement Clone(); + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + protected abstract string GetSignature(ITamperResistantOAuthMessage message); + + /// <summary> + /// Checks whether this binding element applies to this message. + /// </summary> + /// <param name="message">The message that needs to be signed.</param> + /// <returns>True if this binding element can be used to sign the message. False otherwise.</returns> + protected virtual bool IsMessageApplicable(ITamperResistantOAuthMessage message) { + return string.IsNullOrEmpty(message.SignatureMethod) || message.SignatureMethod == this.signatureMethod; + } + + /// <summary> + /// Sorts parameters according to OAuth signature base string rules. + /// </summary> + /// <param name="left">The first parameter to compare.</param> + /// <param name="right">The second parameter to compare.</param> + /// <returns>Negative, zero or positive.</returns> + private static int SignatureBaseStringParameterComparer(KeyValuePair<string, string> left, KeyValuePair<string, string> right) { + int result = string.CompareOrdinal(left.Key, right.Key); + if (result != 0) { + return result; + } + + return string.CompareOrdinal(left.Value, right.Value); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs new file mode 100644 index 0000000..4ff52fd --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementBaseContract.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Code Contract for the <see cref="SigningBindingElementBase"/> class. + /// </summary> + [ContractClassFor(typeof(SigningBindingElementBase))] + internal abstract class SigningBindingElementBaseContract : SigningBindingElementBase { + /// <summary> + /// Prevents a default instance of the SigningBindingElementBaseContract class from being created. + /// </summary> + private SigningBindingElementBaseContract() + : base(string.Empty) { + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + /// <remarks> + /// Implementations of this method need not clone the SignatureVerificationCallback member, as the + /// <see cref="SigningBindingElementBase"/> class does this. + /// </remarks> + protected override ITamperProtectionChannelBindingElement Clone() { + throw new NotImplementedException(); + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + Contract.Requires<InvalidOperationException>(this.Channel != null); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs new file mode 100644 index 0000000..67c5205 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs @@ -0,0 +1,143 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementChain.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A tamper protection applying binding element that can use any of several given + /// binding elements to apply the protection. + /// </summary> + internal class SigningBindingElementChain : ITamperProtectionChannelBindingElement { + /// <summary> + /// The various signing binding elements that may be applicable to a message in preferred use order. + /// </summary> + private readonly ITamperProtectionChannelBindingElement[] signers; + + /// <summary> + /// Initializes a new instance of the <see cref="SigningBindingElementChain"/> class. + /// </summary> + /// <param name="signers"> + /// The signing binding elements that may be used for some outgoing message, + /// in preferred use order. + /// </param> + internal SigningBindingElementChain(ITamperProtectionChannelBindingElement[] signers) { + Contract.Requires<ArgumentNullException>(signers != null); + Contract.Requires<ArgumentException>(signers.Length > 0); + Contract.Requires<ArgumentException>(!signers.Contains(null), MessagingStrings.SequenceContainsNullElement); + Contract.Requires<ArgumentException>(signers.Select(s => s.Protection).Distinct().Count() == 1, OAuthStrings.SigningElementsMustShareSameProtection); + + this.signers = signers; + } + + #region ITamperProtectionChannelBindingElement Properties + + /// <summary> + /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a signed + /// message so that its signature can be correctly calculated for verification. + /// May be null for Consumers (who never have to verify signatures). + /// </summary> + public Action<ITamperResistantOAuthMessage> SignatureCallback { + get { + return this.signers[0].SignatureCallback; + } + + set { + foreach (ITamperProtectionChannelBindingElement signer in this.signers) { + signer.SignatureCallback = value; + } + } + } + + #endregion + + #region IChannelBindingElement Members + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + public MessageProtections Protection { + get { return this.signers[0].Protection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { + get { + return this.signers[0].Channel; + } + + set { + foreach (var signer in this.signers) { + signer.Channel = value; + } + } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + foreach (IChannelBindingElement signer in this.signers) { + ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null."); + MessageProtections? result = signer.ProcessOutgoingMessage(message); + if (result.HasValue) { + return result; + } + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + foreach (IChannelBindingElement signer in this.signers) { + ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null."); + MessageProtections? result = signer.ProcessIncomingMessage(message); + if (result.HasValue) { + return result; + } + } + + return null; + } + + #endregion + + #region ITamperProtectionChannelBindingElement Methods + + /// <summary> + /// Creates a new object that is a copy of the current instance. + /// </summary> + /// <returns> + /// A new object that is a copy of this instance. + /// </returns> + ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() { + return new SigningBindingElementChain(this.signers.Select(el => (ITamperProtectionChannelBindingElement)el.Clone()).ToArray()); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/StandardTokenGenerator.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/StandardTokenGenerator.cs new file mode 100644 index 0000000..d18f5fe --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/StandardTokenGenerator.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardTokenGenerator.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Security.Cryptography; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A cryptographically strong random string generator for tokens and secrets. + /// </summary> + internal class StandardTokenGenerator : ITokenGenerator { + #region ITokenGenerator Members + + /// <summary> + /// Generates a new token to represent a not-yet-authorized request to access protected resources. + /// </summary> + /// <param name="consumerKey">The consumer that requested this token.</param> + /// <returns>The newly generated token.</returns> + /// <remarks> + /// This method should not store the newly generated token in any persistent store. + /// This will be done in <see cref="ITokenManager.StoreNewRequestToken"/>. + /// </remarks> + public string GenerateRequestToken(string consumerKey) { + return GenerateCryptographicallyStrongString(); + } + + /// <summary> + /// Generates a new token to represent an authorized request to access protected resources. + /// </summary> + /// <param name="consumerKey">The consumer that requested this token.</param> + /// <returns>The newly generated token.</returns> + /// <remarks> + /// This method should not store the newly generated token in any persistent store. + /// This will be done in <see cref="ITokenManager.ExpireRequestTokenAndStoreNewAccessToken"/>. + /// </remarks> + public string GenerateAccessToken(string consumerKey) { + return GenerateCryptographicallyStrongString(); + } + + /// <summary> + /// Returns a cryptographically strong random string for use as a token secret. + /// </summary> + /// <returns>The generated string.</returns> + public string GenerateSecret() { + return GenerateCryptographicallyStrongString(); + } + + #endregion + + /// <summary> + /// Returns a new random string. + /// </summary> + /// <returns>The new random string.</returns> + private static string GenerateCryptographicallyStrongString() { + byte[] buffer = new byte[20]; + MessagingUtilities.CryptoRandomDataGenerator.GetBytes(buffer); + return Convert.ToBase64String(buffer); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs new file mode 100644 index 0000000..f53aa1b --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs @@ -0,0 +1,201 @@ +//----------------------------------------------------------------------- +// <copyright file="TokenHandlingBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// A binding element for Service Providers to manage the + /// callbacks and verification codes on applicable messages. + /// </summary> + internal class TokenHandlingBindingElement : IChannelBindingElement { + /// <summary> + /// The token manager offered by the service provider. + /// </summary> + private IServiceProviderTokenManager tokenManager; + + /// <summary> + /// The security settings for this service provider. + /// </summary> + private ServiceProviderSecuritySettings securitySettings; + + /// <summary> + /// Initializes a new instance of the <see cref="TokenHandlingBindingElement"/> class. + /// </summary> + /// <param name="tokenManager">The token manager.</param> + /// <param name="securitySettings">The security settings.</param> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contract"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")] + internal TokenHandlingBindingElement(IServiceProviderTokenManager tokenManager, ServiceProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(tokenManager != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + + this.tokenManager = tokenManager; + this.securitySettings = securitySettings; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var userAuthResponse = message as UserAuthorizationResponse; + if (userAuthResponse != null && userAuthResponse.Version >= Protocol.V10a.Version) { + var requestToken = this.tokenManager.GetRequestToken(userAuthResponse.RequestToken); + requestToken.VerificationCode = userAuthResponse.VerificationCode; + this.tokenManager.UpdateToken(requestToken); + return MessageProtections.None; + } + + // Hook to store the token and secret on its way down to the Consumer. + var grantRequestTokenResponse = message as UnauthorizedTokenResponse; + if (grantRequestTokenResponse != null) { + this.tokenManager.StoreNewRequestToken(grantRequestTokenResponse.RequestMessage, grantRequestTokenResponse); + + // The host may have already set these properties, but just to make sure... + var requestToken = this.tokenManager.GetRequestToken(grantRequestTokenResponse.RequestToken); + requestToken.ConsumerVersion = grantRequestTokenResponse.Version; + if (grantRequestTokenResponse.RequestMessage.Callback != null) { + requestToken.Callback = grantRequestTokenResponse.RequestMessage.Callback; + } + this.tokenManager.UpdateToken(requestToken); + + return MessageProtections.None; + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var authorizedTokenRequest = message as AuthorizedTokenRequest; + if (authorizedTokenRequest != null) { + if (authorizedTokenRequest.Version >= Protocol.V10a.Version) { + string expectedVerifier = this.tokenManager.GetRequestToken(authorizedTokenRequest.RequestToken).VerificationCode; + ErrorUtilities.VerifyProtocol(string.Equals(authorizedTokenRequest.VerificationCode, expectedVerifier, StringComparison.Ordinal), OAuthStrings.IncorrectVerifier); + return MessageProtections.None; + } + + this.VerifyThrowTokenTimeToLive(authorizedTokenRequest); + } + + var userAuthorizationRequest = message as UserAuthorizationRequest; + if (userAuthorizationRequest != null) { + this.VerifyThrowTokenTimeToLive(userAuthorizationRequest); + } + + var accessResourceRequest = message as AccessProtectedResourceRequest; + if (accessResourceRequest != null) { + this.VerifyThrowTokenNotExpired(accessResourceRequest); + } + + return null; + } + + #endregion + + /// <summary> + /// Ensures that access tokens have not yet expired. + /// </summary> + /// <param name="message">The incoming message carrying the access token.</param> + private void VerifyThrowTokenNotExpired(AccessProtectedResourceRequest message) { + Contract.Requires<ArgumentNullException>(message != null); + + try { + IServiceProviderAccessToken token = this.tokenManager.GetAccessToken(message.AccessToken); + if (token.ExpirationDate.HasValue && DateTime.Now >= token.ExpirationDate.Value.ToLocalTimeSafe()) { + Logger.OAuth.ErrorFormat( + "OAuth access token {0} rejected because it expired at {1}, and it is now {2}.", + token.Token, + token.ExpirationDate.Value, + DateTime.Now); + ErrorUtilities.ThrowProtocol(OAuthStrings.TokenNotFound); + } + } catch (KeyNotFoundException ex) { + throw ErrorUtilities.Wrap(ex, OAuthStrings.TokenNotFound); + } + } + + /// <summary> + /// Ensures that short-lived request tokens included in incoming messages have not expired. + /// </summary> + /// <param name="message">The incoming message.</param> + /// <exception cref="ProtocolException">Thrown when the token in the message has expired.</exception> + private void VerifyThrowTokenTimeToLive(ITokenContainingMessage message) { + ErrorUtilities.VerifyInternal(!(message is AccessProtectedResourceRequest), "We shouldn't be verifying TTL on access tokens."); + if (message == null || string.IsNullOrEmpty(message.Token)) { + return; + } + + try { + IServiceProviderRequestToken token = this.tokenManager.GetRequestToken(message.Token); + TimeSpan ttl = this.securitySettings.MaximumRequestTokenTimeToLive; + if (DateTime.Now >= token.CreatedOn.ToLocalTimeSafe() + ttl) { + Logger.OAuth.ErrorFormat( + "OAuth request token {0} rejected because it was originally issued at {1}, expired at {2}, and it is now {3}.", + token.Token, + token.CreatedOn, + token.CreatedOn + ttl, + DateTime.Now); + ErrorUtilities.ThrowProtocol(OAuthStrings.TokenNotFound); + } + } catch (KeyNotFoundException ex) { + throw ErrorUtilities.Wrap(ex, OAuthStrings.TokenNotFound); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs new file mode 100644 index 0000000..46c2bd9 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="TokenType.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + /// <summary> + /// The two types of tokens that exist in the OAuth protocol. + /// </summary> + public enum TokenType { + /// <summary> + /// A token that is freely issued to any known Consumer. + /// It does not grant any authorization to access protected resources, + /// but is used as a step in obtaining that access. + /// </summary> + RequestToken, + + /// <summary> + /// A token only obtained after the owner of some protected resource(s) + /// has approved a Consumer's access to said resource(s). + /// </summary> + AccessToken, + + /// <summary> + /// An unrecognized, expired or invalid token. + /// </summary> + InvalidToken, + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs new file mode 100644 index 0000000..287ef01 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------- +// <copyright file="UriOrOobEncoding.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// An URI encoder that translates null <see cref="Uri"/> references as "oob" + /// instead of an empty/missing argument. + /// </summary> + internal class UriOrOobEncoding : IMessagePartNullEncoder { + /// <summary> + /// The string constant "oob", used to indicate an out-of-band configuration. + /// </summary> + private const string OutOfBandConfiguration = "oob"; + + /// <summary> + /// Initializes a new instance of the <see cref="UriOrOobEncoding"/> class. + /// </summary> + public UriOrOobEncoding() { + } + + #region IMessagePartNullEncoder Members + + /// <summary> + /// Gets the string representation to include in a serialized message + /// when the message part has a <c>null</c> value. + /// </summary> + /// <value></value> + public string EncodedNullValue { + get { return OutOfBandConfiguration; } + } + + #endregion + + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + Uri uriValue = (Uri)value; + return uriValue.AbsoluteUri; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + if (string.Equals(value, OutOfBandConfiguration, StringComparison.Ordinal)) { + return null; + } else { + return new Uri(value, UriKind.Absolute); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ClassDiagram.cd b/src/DotNetOpenAuth.OAuth/OAuth/ClassDiagram.cd new file mode 100644 index 0000000..f56fd83 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ClassDiagram.cd @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1" MembersFormat="NameAndType"> + <Class Name="DotNetOpenAuth.DesktopConsumer"> + <Position X="11.5" Y="5.25" Width="4.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAEAAAAAAABAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\DesktopConsumer.cs</FileName> + <NewMemberFileName>WebConsumer.cs</NewMemberFileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.WebConsumer"> + <Position X="6.25" Y="5.25" Width="5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAA=</HashCode> + <FileName>OAuth\WebConsumer.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.ServiceProviderDescription"> + <Position X="0.5" Y="5.25" Width="5" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAACAAgAAAAEAAAQAAAAAAAAAAAAACAAAAAgAAAAAAA=</HashCode> + <FileName>OAuth\ServiceProviderDescription.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.ServiceProvider"> + <Position X="1" Y="0.5" Width="6" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>EAoAAAsgAAAAIAAAEAAAAAAAAAAIAAEAAQAIAAwAAAA=</HashCode> + <FileName>OAuth\ServiceProvider.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.ConsumerBase"> + <Position X="9" Y="0.5" Width="4.5" /> + <TypeIdentifier> + <HashCode>ACAAAAEgAAAAAAAAAAAABgAAAAAIAAAAAQAAAAABABA=</HashCode> + <FileName>OAuth\ConsumerBase.cs</FileName> + </TypeIdentifier> + </Class> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/ConsumerBase.cs new file mode 100644 index 0000000..d9fa889 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ConsumerBase.cs @@ -0,0 +1,302 @@ +//----------------------------------------------------------------------- +// <copyright file="ConsumerBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Net; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// Base class for <see cref="WebConsumer"/> and <see cref="DesktopConsumer"/> types. + /// </summary> + public class ConsumerBase : IDisposable { + /// <summary> + /// Initializes a new instance of the <see cref="ConsumerBase"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior of the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + protected ConsumerBase(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) { + Contract.Requires<ArgumentNullException>(serviceDescription != null); + Contract.Requires<ArgumentNullException>(tokenManager != null); + + ITamperProtectionChannelBindingElement signingElement = serviceDescription.CreateTamperProtectionElement(); + INonceStore store = new NonceMemoryStore(StandardExpirationBindingElement.MaximumMessageAge); + this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.Consumer.SecuritySettings.CreateSecuritySettings(); + this.OAuthChannel = new OAuthChannel(signingElement, store, tokenManager, this.SecuritySettings); + this.ServiceProvider = serviceDescription; + + Reporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, null); + } + + /// <summary> + /// Gets the Consumer Key used to communicate with the Service Provider. + /// </summary> + public string ConsumerKey { + get { return this.TokenManager.ConsumerKey; } + } + + /// <summary> + /// Gets the Service Provider that will be accessed. + /// </summary> + public ServiceProviderDescription ServiceProvider { get; private set; } + + /// <summary> + /// Gets the persistence store for tokens and secrets. + /// </summary> + public IConsumerTokenManager TokenManager { + get { return (IConsumerTokenManager)this.OAuthChannel.TokenManager; } + } + + /// <summary> + /// Gets the channel to use for sending/receiving messages. + /// </summary> + public Channel Channel { + get { return this.OAuthChannel; } + } + + /// <summary> + /// Gets the security settings for this consumer. + /// </summary> + internal ConsumerSecuritySettings SecuritySettings { get; private set; } + + /// <summary> + /// Gets or sets the channel to use for sending/receiving messages. + /// </summary> + internal OAuthChannel OAuthChannel { get; set; } + + /// <summary> + /// Obtains an access token for a new account at the Service Provider via 2-legged OAuth. + /// </summary> + /// <param name="requestParameters">Any applicable parameters to include in the query string of the token request.</param> + /// <returns>The access token.</returns> + /// <remarks> + /// The token secret is stored in the <see cref="TokenManager"/>. + /// </remarks> + public string RequestNewClientAccount(IDictionary<string, string> requestParameters = null) { + // Obtain an unauthorized request token. Assume the OAuth version given in the service description. + var token = new UnauthorizedTokenRequest(this.ServiceProvider.RequestTokenEndpoint, this.ServiceProvider.Version) { + ConsumerKey = this.ConsumerKey, + }; + var tokenAccessor = this.Channel.MessageDescriptions.GetAccessor(token); + tokenAccessor.AddExtraParameters(requestParameters); + var requestTokenResponse = this.Channel.Request<UnauthorizedTokenResponse>(token); + this.TokenManager.StoreNewRequestToken(token, requestTokenResponse); + + var requestAccess = new AuthorizedTokenRequest(this.ServiceProvider.AccessTokenEndpoint, this.ServiceProvider.Version) { + RequestToken = requestTokenResponse.RequestToken, + ConsumerKey = this.ConsumerKey, + }; + var grantAccess = this.Channel.Request<AuthorizedTokenResponse>(requestAccess); + this.TokenManager.ExpireRequestTokenAndStoreNewAccessToken(this.ConsumerKey, requestTokenResponse.RequestToken, grantAccess.AccessToken, grantAccess.TokenSecret); + return grantAccess.AccessToken; + } + + /// <summary> + /// Creates a web request prepared with OAuth authorization + /// that may be further tailored by adding parameters by the caller. + /// </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> + /// <returns>The initialized WebRequest object.</returns> + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint endpoint, string accessToken) { + Contract.Requires<ArgumentNullException>(endpoint != null); + Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(accessToken)); + + return this.PrepareAuthorizedRequest(endpoint, accessToken, EmptyDictionary<string, string>.Instance); + } + + /// <summary> + /// Creates a web request prepared with OAuth authorization + /// that may be further tailored by adding parameters by the caller. + /// </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="extraData">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, IDictionary<string, string> extraData) { + Contract.Requires<ArgumentNullException>(endpoint != null); + Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(accessToken)); + Contract.Requires<ArgumentNullException>(extraData != null); + + IDirectedProtocolMessage message = this.CreateAuthorizingMessage(endpoint, accessToken); + foreach (var pair in extraData) { + message.ExtraData.Add(pair); + } + + HttpWebRequest wr = this.OAuthChannel.InitializeRequest(message); + return wr; + } + + /// <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> + /// <returns> + /// The HttpWebRequest that can be used to send the HTTP request to the remote service provider. + /// </returns> + /// <remarks> + /// If <see cref="IDirectedProtocolMessage.HttpMethods"/> property on the + /// <paramref name="message"/> has the + /// <see cref="HttpDeliveryMethods.AuthorizationHeaderRequest"/> flag set and + /// <see cref="ITamperResistantOAuthMessage.HttpMethod"/> is set to an HTTP method + /// that includes an entity body, the request stream is automatically sent + /// if and only if the <see cref="IMessage.ExtraData"/> dictionary is non-empty. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Type of parameter forces the method to apply only to specific scenario.")] + public HttpWebRequest PrepareAuthorizedRequest(AccessProtectedResourceRequest message) { + Contract.Requires<ArgumentNullException>(message != null); + return this.OAuthChannel.InitializeRequest(message); + } + + /// <summary> + /// Creates a web request prepared with OAuth authorization + /// that may be further tailored by adding parameters by the caller. + /// </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> + /// <returns>The initialized WebRequest object.</returns> + /// <exception cref="WebException">Thrown if the request fails for any reason after it is sent to the Service Provider.</exception> + public IncomingWebResponse PrepareAuthorizedRequestAndSend(MessageReceivingEndpoint endpoint, string accessToken) { + IDirectedProtocolMessage message = this.CreateAuthorizingMessage(endpoint, accessToken); + HttpWebRequest wr = this.OAuthChannel.InitializeRequest(message); + return this.Channel.WebRequestHandler.GetResponse(wr); + } + + #region IDisposable Members + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// <summary> + /// Creates a web request prepared with OAuth authorization + /// that may be further tailored by adding parameters by the caller. + /// </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> + /// <returns>The initialized WebRequest object.</returns> + protected internal AccessProtectedResourceRequest CreateAuthorizingMessage(MessageReceivingEndpoint endpoint, string accessToken) { + Contract.Requires<ArgumentNullException>(endpoint != null); + Contract.Requires<ArgumentNullException>(!String.IsNullOrEmpty(accessToken)); + + AccessProtectedResourceRequest message = new AccessProtectedResourceRequest(endpoint, this.ServiceProvider.Version) { + AccessToken = accessToken, + ConsumerKey = this.ConsumerKey, + }; + + return message; + } + + /// <summary> + /// Prepares an OAuth message that begins an authorization request that will + /// redirect the user to the Service Provider to provide that authorization. + /// </summary> + /// <param name="callback"> + /// An optional Consumer URL that the Service Provider should redirect the + /// User Agent to upon successful authorization. + /// </param> + /// <param name="requestParameters">Extra parameters to add to the request token message. Optional.</param> + /// <param name="redirectParameters">Extra parameters to add to the redirect to Service Provider message. Optional.</param> + /// <param name="requestToken">The request token that must be exchanged for an access token after the user has provided authorization.</param> + /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "3#", Justification = "Two results")] + protected internal UserAuthorizationRequest PrepareRequestUserAuthorization(Uri callback, IDictionary<string, string> requestParameters, IDictionary<string, string> redirectParameters, out string requestToken) { + // Obtain an unauthorized request token. Assume the OAuth version given in the service description. + var token = new UnauthorizedTokenRequest(this.ServiceProvider.RequestTokenEndpoint, this.ServiceProvider.Version) { + ConsumerKey = this.ConsumerKey, + Callback = callback, + }; + var tokenAccessor = this.Channel.MessageDescriptions.GetAccessor(token); + tokenAccessor.AddExtraParameters(requestParameters); + var requestTokenResponse = this.Channel.Request<UnauthorizedTokenResponse>(token); + this.TokenManager.StoreNewRequestToken(token, requestTokenResponse); + + // Fine-tune our understanding of the SP's supported OAuth version if it's wrong. + if (this.ServiceProvider.Version != requestTokenResponse.Version) { + Logger.OAuth.WarnFormat("Expected OAuth service provider at endpoint {0} to use OAuth {1} but {2} was detected. Adjusting service description to new version.", this.ServiceProvider.RequestTokenEndpoint.Location, this.ServiceProvider.Version, requestTokenResponse.Version); + this.ServiceProvider.ProtocolVersion = Protocol.Lookup(requestTokenResponse.Version).ProtocolVersion; + } + + // Request user authorization. The OAuth version will automatically include + // or drop the callback that we're setting here. + ITokenContainingMessage assignedRequestToken = requestTokenResponse; + var requestAuthorization = new UserAuthorizationRequest(this.ServiceProvider.UserAuthorizationEndpoint, assignedRequestToken.Token, requestTokenResponse.Version) { + Callback = callback, + }; + var requestAuthorizationAccessor = this.Channel.MessageDescriptions.GetAccessor(requestAuthorization); + requestAuthorizationAccessor.AddExtraParameters(redirectParameters); + requestToken = requestAuthorization.RequestToken; + return requestAuthorization; + } + + /// <summary> + /// Exchanges a given request token for access token. + /// </summary> + /// <param name="requestToken">The request token that the user has authorized.</param> + /// <param name="verifier">The verifier code.</param> + /// <returns> + /// The access token assigned by the Service Provider. + /// </returns> + protected AuthorizedTokenResponse ProcessUserAuthorization(string requestToken, string verifier) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(requestToken)); + Contract.Ensures(Contract.Result<AuthorizedTokenResponse>() != null); + + var requestAccess = new AuthorizedTokenRequest(this.ServiceProvider.AccessTokenEndpoint, this.ServiceProvider.Version) { + RequestToken = requestToken, + VerificationCode = verifier, + ConsumerKey = this.ConsumerKey, + }; + var grantAccess = this.Channel.Request<AuthorizedTokenResponse>(requestAccess); + this.TokenManager.ExpireRequestTokenAndStoreNewAccessToken(this.ConsumerKey, requestToken, grantAccess.AccessToken, grantAccess.TokenSecret); + return grantAccess; + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + if (disposing) { + this.Channel.Dispose(); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ConsumerSecuritySettings.cs b/src/DotNetOpenAuth.OAuth/OAuth/ConsumerSecuritySettings.cs new file mode 100644 index 0000000..bb2fbaa --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ConsumerSecuritySettings.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="ConsumerSecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + /// <summary> + /// Security settings that are applicable to consumers. + /// </summary> + internal class ConsumerSecuritySettings : SecuritySettings { + /// <summary> + /// Initializes a new instance of the <see cref="ConsumerSecuritySettings"/> class. + /// </summary> + internal ConsumerSecuritySettings() { + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/DesktopConsumer.cs b/src/DotNetOpenAuth.OAuth/OAuth/DesktopConsumer.cs new file mode 100644 index 0000000..f9c1a94 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/DesktopConsumer.cs @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------- +// <copyright file="DesktopConsumer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// Used by a desktop application to use OAuth to access the Service Provider on behalf of the User. + /// </summary> + /// <remarks> + /// The methods on this class are thread-safe. Provided the properties are set and not changed + /// afterward, a single instance of this class may be used by an entire desktop application safely. + /// </remarks> + public class DesktopConsumer : ConsumerBase { + /// <summary> + /// Initializes a new instance of the <see cref="DesktopConsumer"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior of the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + public DesktopConsumer(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) + : base(serviceDescription, tokenManager) { + } + + /// <summary> + /// Begins an OAuth authorization request. + /// </summary> + /// <param name="requestParameters">Extra parameters to add to the request token message. Optional.</param> + /// <param name="redirectParameters">Extra parameters to add to the redirect to Service Provider message. Optional.</param> + /// <param name="requestToken">The request token that must be exchanged for an access token after the user has provided authorization.</param> + /// <returns>The URL to open a browser window to allow the user to provide authorization.</returns> + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Two results")] + public Uri RequestUserAuthorization(IDictionary<string, string> requestParameters, IDictionary<string, string> redirectParameters, out string requestToken) { + var message = this.PrepareRequestUserAuthorization(null, requestParameters, redirectParameters, out requestToken); + OutgoingWebResponse response = this.Channel.PrepareResponse(message); + return response.GetDirectUriRequest(this.Channel); + } + + /// <summary> + /// Exchanges a given request token for access token. + /// </summary> + /// <param name="requestToken">The request token that the user has authorized.</param> + /// <returns>The access token assigned by the Service Provider.</returns> + [Obsolete("Use the ProcessUserAuthorization method that takes a verifier parameter instead.")] + public AuthorizedTokenResponse ProcessUserAuthorization(string requestToken) { + return this.ProcessUserAuthorization(requestToken, null); + } + + /// <summary> + /// Exchanges a given request token for access token. + /// </summary> + /// <param name="requestToken">The request token that the user has authorized.</param> + /// <param name="verifier">The verifier code typed in by the user. Must not be <c>Null</c> for OAuth 1.0a service providers and later.</param> + /// <returns> + /// The access token assigned by the Service Provider. + /// </returns> + public new AuthorizedTokenResponse ProcessUserAuthorization(string requestToken, string verifier) { + if (this.ServiceProvider.Version >= Protocol.V10a.Version) { + ErrorUtilities.VerifyNonZeroLength(verifier, "verifier"); + } + + return base.ProcessUserAuthorization(requestToken, verifier); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/AccessProtectedResourceRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AccessProtectedResourceRequest.cs new file mode 100644 index 0000000..f3231f0 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AccessProtectedResourceRequest.cs @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------- +// <copyright file="AccessProtectedResourceRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// 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, 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> + /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> + /// <param name="version">The OAuth version.</param> + protected internal AccessProtectedResourceRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.AccessToken; } + set { this.AccessToken = value; } + } + + /// <summary> + /// Gets or sets the Access Token. + /// </summary> + /// <remarks> + /// In addition to just allowing OAuth to verify a valid message, + /// this property is useful on the Service Provider to verify that the access token + /// has proper authorization for the resource being requested, and to know the + /// context around which user provided the authorization. + /// </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/OAuth/Messages/AuthorizedTokenRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenRequest.cs new file mode 100644 index 0000000..02c6c1d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenRequest.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizedTokenRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Globalization; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A direct message sent by the Consumer to exchange an authorized Request Token + /// for an Access Token and Token Secret. + /// </summary> + /// <remarks> + /// The class is sealed because the OAuth spec forbids adding parameters to this message. + /// </remarks> + public sealed class AuthorizedTokenRequest : SignedMessageBase, ITokenContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizedTokenRequest"/> class. + /// </summary> + /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> + /// <param name="version">The OAuth version.</param> + internal AuthorizedTokenRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the Token. + /// </summary> + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <summary> + /// Gets or sets the verification code received by the Consumer from the Service Provider + /// in the <see cref="UserAuthorizationResponse.VerificationCode"/> property. + /// </summary> + [MessagePart("oauth_verifier", IsRequired = true, AllowEmpty = false, MinVersion = Protocol.V10aVersion)] + public string VerificationCode { get; set; } + + /// <summary> + /// Gets or sets the authorized Request Token used to obtain authorization. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + internal string RequestToken { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.ExtraData.Count > 0) { + throw new ProtocolException(string.Format(CultureInfo.CurrentCulture, OAuthStrings.MessageNotAllowedExtraParameters, GetType().Name)); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenResponse.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenResponse.cs new file mode 100644 index 0000000..0b14819 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenResponse.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizedTokenResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A direct message sent from Service Provider to Consumer in response to + /// a Consumer's <see cref="AuthorizedTokenRequest"/> request. + /// </summary> + public class AuthorizedTokenResponse : MessageBase, ITokenSecretContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizedTokenResponse"/> class. + /// </summary> + /// <param name="originatingRequest">The originating request.</param> + protected internal AuthorizedTokenResponse(AuthorizedTokenRequest originatingRequest) + : base(MessageProtections.None, originatingRequest, originatingRequest.Version) { + } + + /// <summary> + /// Gets or sets the Access Token assigned by the Service Provider. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + public string AccessToken { get; set; } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.AccessToken; } + set { this.AccessToken = value; } + } + + /// <summary> + /// Gets or sets the Request or Access Token secret. + /// </summary> + string ITokenSecretContainingMessage.TokenSecret { + get { return this.TokenSecret; } + set { this.TokenSecret = value; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + + /// <summary> + /// Gets or sets the Token Secret. + /// </summary> + [MessagePart("oauth_token_secret", IsRequired = true)] + protected internal string TokenSecret { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenContainingMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenContainingMessage.cs new file mode 100644 index 0000000..832b754 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenContainingMessage.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenContainingMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + /// <summary> + /// An interface implemented by all OAuth messages that have a request or access token property. + /// </summary> + public interface ITokenContainingMessage { + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + string Token { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenSecretContainingMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenSecretContainingMessage.cs new file mode 100644 index 0000000..95809ec --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenSecretContainingMessage.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenSecretContainingMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + /// <summary> + /// An interface implemented by all OAuth messages that have a request or access token and secret properties. + /// </summary> + public interface ITokenSecretContainingMessage : ITokenContainingMessage { + /// <summary> + /// Gets or sets the Request or Access Token secret. + /// </summary> + string TokenSecret { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/MessageBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/MessageBase.cs new file mode 100644 index 0000000..1a0ba23 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/MessageBase.cs @@ -0,0 +1,281 @@ +//----------------------------------------------------------------------- +// <copyright file="MessageBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A base class for all OAuth messages. + /// </summary> + [Serializable] + public abstract class MessageBase : IDirectedProtocolMessage, IDirectResponseProtocolMessage { + /// <summary> + /// A store for extra name/value data pairs that are attached to this message. + /// </summary> + private Dictionary<string, string> extraData = new Dictionary<string, string>(); + + /// <summary> + /// Gets a value indicating whether signing this message is required. + /// </summary> + private MessageProtections protectionRequired; + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + private MessageTransport transport; + + /// <summary> + /// The URI to the remote endpoint to send this message to. + /// </summary> + private MessageReceivingEndpoint recipient; + + /// <summary> + /// Backing store for the <see cref="OriginatingRequest"/> properties. + /// </summary> + private IDirectedProtocolMessage originatingRequest; + + /// <summary> + /// Backing store for the <see cref="Incoming"/> properties. + /// </summary> + private bool incoming; + +#if DEBUG + /// <summary> + /// Initializes static members of the <see cref="MessageBase"/> class. + /// </summary> + static MessageBase() { + LowSecurityMode = true; + } +#endif + + /// <summary> + /// Initializes a new instance of the <see cref="MessageBase"/> class for direct response messages. + /// </summary> + /// <param name="protectionRequired">The level of protection the message requires.</param> + /// <param name="originatingRequest">The request that asked for this direct response.</param> + /// <param name="version">The OAuth version.</param> + protected MessageBase(MessageProtections protectionRequired, IDirectedProtocolMessage originatingRequest, Version version) { + Contract.Requires<ArgumentNullException>(originatingRequest != null); + Contract.Requires<ArgumentNullException>(version != null); + + this.protectionRequired = protectionRequired; + this.transport = MessageTransport.Direct; + this.originatingRequest = originatingRequest; + this.Version = version; + } + + /// <summary> + /// Initializes a new instance of the <see cref="MessageBase"/> class for direct requests or indirect messages. + /// </summary> + /// <param name="protectionRequired">The level of protection the message requires.</param> + /// <param name="transport">A value indicating whether this message requires a direct or indirect transport.</param> + /// <param name="recipient">The URI that a directed message will be delivered to.</param> + /// <param name="version">The OAuth version.</param> + protected MessageBase(MessageProtections protectionRequired, MessageTransport transport, MessageReceivingEndpoint recipient, Version version) { + Contract.Requires<ArgumentNullException>(recipient != null); + Contract.Requires<ArgumentNullException>(version != null); + + this.protectionRequired = protectionRequired; + this.transport = transport; + this.recipient = recipient; + this.Version = version; + } + + #region IProtocolMessage Properties + + /// <summary> + /// Gets the version of the protocol this message is prepared to implement. + /// </summary> + Version IMessage.Version { + get { return this.Version; } + } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + MessageProtections IProtocolMessage.RequiredProtection { + get { return this.RequiredProtection; } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + MessageTransport IProtocolMessage.Transport { + get { return this.Transport; } + } + + /// <summary> + /// Gets the dictionary of additional name/value fields tacked on to this message. + /// </summary> + IDictionary<string, string> IMessage.ExtraData { + get { return this.ExtraData; } + } + + #endregion + + #region IDirectedProtocolMessage Members + + /// <summary> + /// Gets the URI to the Service Provider endpoint to send this message to. + /// </summary> + Uri IDirectedProtocolMessage.Recipient { + get { return this.recipient != null ? this.recipient.Location : null; } + } + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + HttpDeliveryMethods IDirectedProtocolMessage.HttpMethods { + get { return this.HttpMethods; } + } + + #endregion + + #region IDirectResponseProtocolMessage Members + + /// <summary> + /// Gets the originating request message that caused this response to be formed. + /// </summary> + IDirectedProtocolMessage IDirectResponseProtocolMessage.OriginatingRequest { + get { return this.originatingRequest; } + } + + #endregion + + /// <summary> + /// Gets or sets a value indicating whether security sensitive strings are + /// emitted from the ToString() method. + /// </summary> + internal static bool LowSecurityMode { get; set; } + + /// <summary> + /// Gets a value indicating whether this message was deserialized as an incoming message. + /// </summary> + protected internal bool Incoming { + get { return this.incoming; } + } + + /// <summary> + /// Gets the version of the protocol this message is prepared to implement. + /// </summary> + protected internal Version Version { get; private set; } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + protected MessageProtections RequiredProtection { + get { return this.protectionRequired; } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + protected MessageTransport Transport { + get { return this.transport; } + } + + /// <summary> + /// Gets the dictionary of additional name/value fields tacked on to this message. + /// </summary> + protected IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + protected HttpDeliveryMethods HttpMethods { + get { return this.recipient != null ? this.recipient.AllowedMethods : HttpDeliveryMethods.None; } + } + + /// <summary> + /// Gets or sets the URI to the Service Provider endpoint to send this message to. + /// </summary> + protected Uri Recipient { + get { + return this.recipient != null ? this.recipient.Location : null; + } + + set { + if (this.recipient != null) { + this.recipient = new MessageReceivingEndpoint(value, this.recipient.AllowedMethods); + } else if (value != null) { + throw new InvalidOperationException(); + } + } + } + + /// <summary> + /// Gets the originating request message that caused this response to be formed. + /// </summary> + protected IDirectedProtocolMessage OriginatingRequest { + get { return this.originatingRequest; } + } + + #region IProtocolMessage Methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + void IMessage.EnsureValidMessage() { + this.EnsureValidMessage(); + } + + #endregion + + /// <summary> + /// Returns a human-friendly string describing the message and all serializable properties. + /// </summary> + /// <param name="channel">The channel that will carry this message.</param> + /// <returns> + /// The string representation of this object. + /// </returns> + internal virtual string ToString(Channel channel) { + Contract.Requires<ArgumentNullException>(channel != null); + + StringBuilder builder = new StringBuilder(); + builder.AppendFormat(CultureInfo.InvariantCulture, "{0} message", GetType().Name); + if (this.recipient != null) { + builder.AppendFormat(CultureInfo.InvariantCulture, " as {0} to {1}", this.recipient.AllowedMethods, this.recipient.Location); + } + builder.AppendLine(); + MessageDictionary dictionary = channel.MessageDescriptions.GetAccessor(this); + foreach (var pair in dictionary) { + string value = pair.Value; + if (pair.Key == "oauth_signature" && !LowSecurityMode) { + value = "xxxxxxxxxxxxx (not shown)"; + } + builder.Append('\t'); + builder.Append(pair.Key); + builder.Append(": "); + builder.AppendLine(value); + } + + return builder.ToString(); + } + + /// <summary> + /// Sets a flag indicating that this message is received (as opposed to sent). + /// </summary> + internal void SetAsIncoming() { + this.incoming = true; + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + protected virtual void EnsureValidMessage() { } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/OAuth Messages.cd b/src/DotNetOpenAuth.OAuth/OAuth/Messages/OAuth Messages.cd new file mode 100644 index 0000000..f5526d2 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/OAuth Messages.cd @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1" GroupingSetting="Access"> + <Comment CommentText="Messages from Service Provider"> + <Position X="7.912" Y="0.715" Height="0.291" Width="2.02" /> + </Comment> + <Comment CommentText="Messages from Consumer"> + <Position X="4.36" Y="0.683" Height="0.291" Width="2.02" /> + </Comment> + <Class Name="DotNetOpenAuth.OAuth.Messages.UnauthorizedTokenRequest"> + <Position X="4.25" Y="1" Width="3" /> + <Compartments> + <Compartment Name="Internal" Collapsed="true" /> + <Compartment Name="Private" Collapsed="true" /> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\UnauthorizedTokenRequest.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.UnauthorizedTokenResponse"> + <Position X="7.5" Y="1.5" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + <Property Name="ITokenSecretContainingMessage.TokenSecret" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAEAAAAAAIAAIAAAIAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\UnauthorizedTokenResponse.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.SignedMessageBase"> + <Position X="0.5" Y="1.5" Width="3.5" /> + <Members> + <Property Name="ITamperResistantOAuthMessage.ConsumerSecret" Hidden="true" /> + <Property Name="ITamperResistantOAuthMessage.HttpMethod" Hidden="true" /> + <Property Name="ITamperResistantOAuthMessage.SignatureMethod" Hidden="true" /> + <Property Name="ITamperResistantOAuthMessage.TokenSecret" Hidden="true" /> + <Property Name="ITamperResistantProtocolMessage.Signature" Hidden="true" /> + </Members> + <TypeIdentifier> + <HashCode>AIAAFAAAAIAAAAACgICAAgAAAgAAIAQAAAEAIgAQAAE=</HashCode> + <FileName>OAuth\Messages\SignedMessageBase.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.UserAuthorizationRequest"> + <Position X="4.25" Y="3" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAAIAAAAAAIAAAAAAAQAA=</HashCode> + <FileName>OAuth\Messages\UserAuthorizationRequest.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.UserAuthorizationResponse"> + <Position X="7.5" Y="4.5" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\UserAuthorizationResponse.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.AuthorizedTokenResponse"> + <Position X="7.5" Y="6.25" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + <Property Name="ITokenSecretContainingMessage.TokenSecret" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAEAAAIAAAIAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\AuthorizedTokenResponse.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.AuthorizedTokenRequest"> + <Position X="4.25" Y="5.5" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACQAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\AuthorizedTokenRequest.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.AccessProtectedResourceRequest"> + <Position X="4.25" Y="7.75" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\AccessProtectedResourceRequest.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.MessageBase"> + <Position X="11" Y="1" Width="3.5" /> + <Members> + <Field Name="extraData" Hidden="true" /> + <Property Name="IDirectedProtocolMessage.Recipient" Hidden="true" /> + <Method Name="IMessage.EnsureValidMessage" Hidden="true" /> + <Property Name="IMessage.ExtraData" Hidden="true" /> + <Property Name="IMessage.Version" Hidden="true" /> + <Property Name="IProtocolMessage.RequiredProtection" Hidden="true" /> + <Property Name="IProtocolMessage.Transport" Hidden="true" /> + <Field Name="protectionRequired" Hidden="true" /> + <Field Name="recipient" Hidden="true" /> + <Field Name="transport" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>ICAMAAQAYAAAgAkEAIAIAAYACgAEYBAAIECBACAAAAA=</HashCode> + <FileName>OAuth\Messages\MessageBase.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Interface Name="DotNetOpenAuth.ChannelElements.ITamperResistantOAuthMessage"> + <Position X="11.25" Y="5.25" Width="2.5" /> + <TypeIdentifier> + <HashCode>AIAAAAAAAAAAAAAAAIAAAgAAAAAAIAQAAAAAAAAAAAA=</HashCode> + <FileName>ChannelElements\ITamperResistantOAuthMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.OAuth.Messages.ITokenContainingMessage"> + <Position X="1" Y="6" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAA=</HashCode> + <FileName>OAuth\Messages\ITokenContainingMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.OAuth.Messages.ITokenSecretContainingMessage"> + <Position X="1" Y="7.5" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\ITokenSecretContainingMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/SignedMessageBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/SignedMessageBase.cs new file mode 100644 index 0000000..6084fc0 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/SignedMessageBase.cs @@ -0,0 +1,197 @@ +//----------------------------------------------------------------------- +// <copyright file="SignedMessageBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A base class for all signed OAuth messages. + /// </summary> + public class SignedMessageBase : MessageBase, ITamperResistantOAuthMessage, IExpiringProtocolMessage, IReplayProtectedProtocolMessage { + /// <summary> + /// The reference date and time for calculating time stamps. + /// </summary> + private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// <summary> + /// The number of seconds since 1/1/1970, consistent with the OAuth timestamp requirement. + /// </summary> + [MessagePart("oauth_timestamp", IsRequired = true)] + private long timestamp; + + /// <summary> + /// Initializes a new instance of the <see cref="SignedMessageBase"/> class. + /// </summary> + /// <param name="transport">A value indicating whether this message requires a direct or indirect transport.</param> + /// <param name="recipient">The URI that a directed message will be delivered to.</param> + /// <param name="version">The OAuth version.</param> + internal SignedMessageBase(MessageTransport transport, MessageReceivingEndpoint recipient, Version version) + : base(MessageProtections.All, transport, recipient, version) { + ITamperResistantOAuthMessage self = (ITamperResistantOAuthMessage)this; + HttpDeliveryMethods methods = ((IDirectedProtocolMessage)this).HttpMethods; + self.HttpMethod = MessagingUtilities.GetHttpVerb(methods); + } + + #region ITamperResistantOAuthMessage Members + + /// <summary> + /// Gets or sets the signature method used to sign the request. + /// </summary> + string ITamperResistantOAuthMessage.SignatureMethod { + get { return this.SignatureMethod; } + set { this.SignatureMethod = value; } + } + + /// <summary> + /// Gets or sets the Token Secret used to sign the message. + /// </summary> + string ITamperResistantOAuthMessage.TokenSecret { + get { return this.TokenSecret; } + set { this.TokenSecret = value; } + } + + /// <summary> + /// Gets or sets the Consumer key. + /// </summary> + [MessagePart("oauth_consumer_key", IsRequired = true)] + public string ConsumerKey { get; set; } + + /// <summary> + /// Gets or sets the Consumer Secret used to sign the message. + /// </summary> + string ITamperResistantOAuthMessage.ConsumerSecret { + get { return this.ConsumerSecret; } + set { this.ConsumerSecret = value; } + } + + /// <summary> + /// Gets or sets the HTTP method that will be used to transmit the message. + /// </summary> + string ITamperResistantOAuthMessage.HttpMethod { + get { return this.HttpMethod; } + set { this.HttpMethod = value; } + } + + /// <summary> + /// Gets or sets the URI to the Service Provider endpoint to send this message to. + /// </summary> + Uri ITamperResistantOAuthMessage.Recipient { + get { return this.Recipient; } + set { this.Recipient = value; } + } + + #endregion + + #region ITamperResistantProtocolMessage Members + + /// <summary> + /// Gets or sets the message signature. + /// </summary> + string ITamperResistantProtocolMessage.Signature { + get { return this.Signature; } + set { this.Signature = value; } + } + + #endregion + + #region IExpiringProtocolMessage Members + + /// <summary> + /// Gets or sets the OAuth timestamp of the message. + /// </summary> + DateTime IExpiringProtocolMessage.UtcCreationDate { + get { return epoch + TimeSpan.FromSeconds(this.timestamp); } + set { this.timestamp = (long)(value - epoch).TotalSeconds; } + } + + #endregion + + #region IReplayProtectedProtocolMessage Members + + /// <summary> + /// Gets the context within which the nonce must be unique. + /// </summary> + /// <value>The consumer key.</value> + string IReplayProtectedProtocolMessage.NonceContext { + get { return this.ConsumerKey; } + } + + /// <summary> + /// Gets or sets the message nonce used for replay detection. + /// </summary> + [MessagePart("oauth_nonce", IsRequired = true)] + string IReplayProtectedProtocolMessage.Nonce { get; set; } + + #endregion + + #region IMessageOriginalPayload Members + + /// <summary> + /// Gets or sets the original message parts, before any normalization or default values were assigned. + /// </summary> + IDictionary<string, string> IMessageOriginalPayload.OriginalPayload { + get { return this.OriginalPayload; } + set { this.OriginalPayload = value; } + } + + /// <summary> + /// Gets or sets the original message parts, before any normalization or default values were assigned. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "By design")] + protected IDictionary<string, string> OriginalPayload { get; set; } + + #endregion + + /// <summary> + /// Gets or sets the signature method used to sign the request. + /// </summary> + [MessagePart("oauth_signature_method", IsRequired = true)] + protected string SignatureMethod { get; set; } + + /// <summary> + /// Gets or sets the Token Secret used to sign the message. + /// </summary> + protected string TokenSecret { get; set; } + + /// <summary> + /// Gets or sets the Consumer Secret used to sign the message. + /// </summary> + protected string ConsumerSecret { get; set; } + + /// <summary> + /// Gets or sets the HTTP method that will be used to transmit the message. + /// </summary> + protected string HttpMethod { get; set; } + + /// <summary> + /// Gets or sets the message signature. + /// </summary> + [MessagePart("oauth_signature", IsRequired = true)] + protected string Signature { get; set; } + + /// <summary> + /// Gets or sets the version of the protocol this message was created with. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Accessed via reflection.")] + [MessagePart("oauth_version", IsRequired = false)] + private string OAuthVersion { + get { + return Protocol.Lookup(Version).PublishedVersion; + } + + set { + if (value != this.OAuthVersion) { + throw new ArgumentOutOfRangeException("value"); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenRequest.cs new file mode 100644 index 0000000..9214d91 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenRequest.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// <copyright file="UnauthorizedTokenRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A direct message sent from Consumer to Service Provider to request a Request Token. + /// </summary> + public class UnauthorizedTokenRequest : SignedMessageBase { + /// <summary> + /// Initializes a new instance of the <see cref="UnauthorizedTokenRequest"/> class. + /// </summary> + /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> + /// <param name="version">The OAuth version.</param> + protected internal UnauthorizedTokenRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the absolute URL to which the Service Provider will redirect the + /// User back when the Obtaining User Authorization step is completed. + /// </summary> + /// <value> + /// The callback URL; or <c>null</c> if the Consumer is unable to receive + /// callbacks or a callback URL has been established via other means. + /// </value> + [MessagePart("oauth_callback", IsRequired = true, AllowEmpty = false, MinVersion = Protocol.V10aVersion, Encoder = typeof(UriOrOobEncoding))] + public Uri Callback { get; set; } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenResponse.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenResponse.cs new file mode 100644 index 0000000..0be9f63 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenResponse.cs @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------- +// <copyright file="UnauthorizedTokenResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A direct message sent from Service Provider to Consumer in response to + /// a Consumer's <see cref="UnauthorizedTokenRequest"/> request. + /// </summary> + public class UnauthorizedTokenResponse : MessageBase, ITokenSecretContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="UnauthorizedTokenResponse"/> class. + /// </summary> + /// <param name="requestMessage">The unauthorized request token message that this message is being generated in response to.</param> + /// <param name="requestToken">The request token.</param> + /// <param name="tokenSecret">The token secret.</param> + /// <remarks> + /// This constructor is used by the Service Provider to send the message. + /// </remarks> + protected internal UnauthorizedTokenResponse(UnauthorizedTokenRequest requestMessage, string requestToken, string tokenSecret) + : this(requestMessage, requestMessage.Version) { + Contract.Requires<ArgumentNullException>(requestToken != null); + Contract.Requires<ArgumentNullException>(tokenSecret != null); + + this.RequestToken = requestToken; + this.TokenSecret = tokenSecret; + } + + /// <summary> + /// Initializes a new instance of the <see cref="UnauthorizedTokenResponse"/> class. + /// </summary> + /// <param name="originatingRequest">The originating request.</param> + /// <param name="version">The OAuth version.</param> + /// <remarks>This constructor is used by the consumer to deserialize the message.</remarks> + protected internal UnauthorizedTokenResponse(UnauthorizedTokenRequest originatingRequest, Version version) + : base(MessageProtections.None, originatingRequest, version) { + } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <summary> + /// Gets or sets the Request or Access Token secret. + /// </summary> + string ITokenSecretContainingMessage.TokenSecret { + get { return this.TokenSecret; } + set { this.TokenSecret = value; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + + /// <summary> + /// Gets or sets the Request Token. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + internal string RequestToken { get; set; } + + /// <summary> + /// Gets the original request for an unauthorized token. + /// </summary> + internal UnauthorizedTokenRequest RequestMessage { + get { return (UnauthorizedTokenRequest)this.OriginatingRequest; } + } + + /// <summary> + /// Gets or sets the Token Secret. + /// </summary> + [MessagePart("oauth_token_secret", IsRequired = true)] + protected internal string TokenSecret { get; set; } + + /// <summary> + /// Gets a value indicating whether the Service Provider recognized the callback parameter in the request. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Message serialization invoked.")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Message parts must be instance members.")] + [MessagePart("oauth_callback_confirmed", IsRequired = true, MinVersion = Protocol.V10aVersion)] + private bool CallbackConfirmed { + get { return true; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationRequest.cs new file mode 100644 index 0000000..a5823bb --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationRequest.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------- +// <copyright file="UserAuthorizationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message used to redirect the user from a Consumer to a Service Provider's web site + /// so the Service Provider can ask the user to authorize the Consumer's access to some + /// protected resource(s). + /// </summary> + [Serializable] + public class UserAuthorizationRequest : MessageBase, ITokenContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="UserAuthorizationRequest"/> class. + /// </summary> + /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> + /// <param name="requestToken">The request token.</param> + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, string requestToken, Version version) + : this(serviceProvider, version) { + this.RequestToken = requestToken; + } + + /// <summary> + /// Initializes a new instance of the <see cref="UserAuthorizationRequest"/> class. + /// </summary> + /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageProtections.None, MessageTransport.Indirect, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + + /// <summary> + /// Gets a value indicating whether this is a safe OAuth authorization request. + /// </summary> + /// <value><c>true</c> if the Consumer is using OAuth 1.0a or later; otherwise, <c>false</c>.</value> + public bool IsUnsafeRequest { + get { return this.Version < Protocol.V10a.Version; } + } + + /// <summary> + /// Gets or sets the Request Token obtained in the previous step. + /// </summary> + /// <remarks> + /// The Service Provider MAY declare this parameter as REQUIRED, or + /// accept requests to the User Authorization URL without it, in which + /// case it will prompt the User to enter it manually. + /// </remarks> + [MessagePart("oauth_token", IsRequired = false)] + internal string RequestToken { get; set; } + + /// <summary> + /// Gets or sets a URL the Service Provider will use to redirect the User back + /// to the Consumer when Obtaining User Authorization is complete. Optional. + /// </summary> + [MessagePart("oauth_callback", IsRequired = false, MaxVersion = "1.0")] + internal Uri Callback { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationResponse.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationResponse.cs new file mode 100644 index 0000000..73fddc7 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationResponse.cs @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------- +// <copyright file="UserAuthorizationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message used to redirect the user from a Service Provider to a Consumer's web site. + /// </summary> + /// <remarks> + /// The class is sealed because extra parameters are determined by the callback URI provided by the Consumer. + /// </remarks> + [Serializable] + public sealed class UserAuthorizationResponse : MessageBase, ITokenContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="UserAuthorizationResponse"/> class. + /// </summary> + /// <param name="consumer">The URI of the Consumer endpoint to send this message to.</param> + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationResponse(Uri consumer, Version version) + : base(MessageProtections.None, MessageTransport.Indirect, new MessageReceivingEndpoint(consumer, HttpDeliveryMethods.GetRequest), version) { + } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <summary> + /// Gets or sets the verification code that must accompany the request to exchange the + /// authorized request token for an access token. + /// </summary> + /// <value>An unguessable value passed to the Consumer via the User and REQUIRED to complete the process.</value> + /// <remarks> + /// If the Consumer did not provide a callback URL, the Service Provider SHOULD display the value of the + /// verification code, and instruct the User to manually inform the Consumer that authorization is + /// completed. If the Service Provider knows a Consumer to be running on a mobile device or set-top box, + /// the Service Provider SHOULD ensure that the verifier value is suitable for manual entry. + /// </remarks> + [MessagePart("oauth_verifier", IsRequired = true, AllowEmpty = false, MinVersion = Protocol.V10aVersion)] + public string VerificationCode { get; set; } + + /// <summary> + /// Gets or sets the Request Token. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + internal string RequestToken { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.Designer.cs b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.Designer.cs new file mode 100644 index 0000000..723839d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.Designer.cs @@ -0,0 +1,198 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.225 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.OAuth { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class OAuthStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal OAuthStrings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.OAuth.OAuthStrings", typeof(OAuthStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot send access token to Consumer for request token '{0}' before it has been authorized.. + /// </summary> + internal static string AccessTokenNotAuthorized { + get { + return ResourceManager.GetString("AccessTokenNotAuthorized", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The access token '{0}' is invalid or expired.. + /// </summary> + internal static string BadAccessTokenInProtectedResourceRequest { + get { + return ResourceManager.GetString("BadAccessTokenInProtectedResourceRequest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failure looking up secret for consumer or token.. + /// </summary> + internal static string ConsumerOrTokenSecretNotFound { + get { + return ResourceManager.GetString("ConsumerOrTokenSecretNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to oauth_verifier argument was incorrect.. + /// </summary> + internal static string IncorrectVerifier { + get { + return ResourceManager.GetString("IncorrectVerifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An invalid OAuth message received and discarded.. + /// </summary> + internal static string InvalidIncomingMessage { + get { + return ResourceManager.GetString("InvalidIncomingMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The {0} message included extra data which is not allowed.. + /// </summary> + internal static string MessageNotAllowedExtraParameters { + get { + return ResourceManager.GetString("MessageNotAllowedExtraParameters", 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}.. + /// </summary> + internal static string MinimumConsumerVersionRequirementNotMet { + get { + return ResourceManager.GetString("MinimumConsumerVersionRequirementNotMet", resourceCulture); + } + } + + /// <summary> + /// 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 MultipartPostMustBeUsedWithAuthHeader { + get { + return ResourceManager.GetString("MultipartPostMustBeUsedWithAuthHeader", resourceCulture); + } + } + + /// <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.. + /// </summary> + internal static string OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface { + get { + 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); + } + } + + /// <summary> + /// Looks up a localized string similar to The request URL query MUST NOT contain any OAuth Protocol Parameters.. + /// </summary> + internal static string RequestUrlMustNotHaveOAuthParameters { + get { + return ResourceManager.GetString("RequestUrlMustNotHaveOAuthParameters", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The signing element already has been associated with a channel.. + /// </summary> + internal static string SigningElementAlreadyAssociatedWithChannel { + get { + return ResourceManager.GetString("SigningElementAlreadyAssociatedWithChannel", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to All signing elements must offer the same message protection.. + /// </summary> + internal static string SigningElementsMustShareSameProtection { + get { + return ResourceManager.GetString("SigningElementsMustShareSameProtection", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A token in the message was not recognized by the service provider.. + /// </summary> + internal static string TokenNotFound { + get { + return ResourceManager.GetString("TokenNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The RSA-SHA1 signing binding element has not been set with a certificate for signing.. + /// </summary> + internal static string X509CertificateNotProvidedForSigning { + get { + return ResourceManager.GetString("X509CertificateNotProvidedForSigning", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.resx b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.resx new file mode 100644 index 0000000..34b314b --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.resx @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AccessTokenNotAuthorized" xml:space="preserve"> + <value>Cannot send access token to Consumer for request token '{0}' before it has been authorized.</value> + </data> + <data name="BadAccessTokenInProtectedResourceRequest" xml:space="preserve"> + <value>The access token '{0}' is invalid or expired.</value> + </data> + <data name="ConsumerOrTokenSecretNotFound" xml:space="preserve"> + <value>Failure looking up secret for consumer or token.</value> + </data> + <data name="IncorrectVerifier" xml:space="preserve"> + <value>oauth_verifier argument was incorrect.</value> + </data> + <data name="InvalidIncomingMessage" xml:space="preserve"> + <value>An invalid OAuth message received and discarded.</value> + </data> + <data name="MessageNotAllowedExtraParameters" xml:space="preserve"> + <value>The {0} message included extra data which is not allowed.</value> + </data> + <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> + <data name="OpenIdOAuthRealmConsumerKeyDoNotMatch" xml:space="preserve"> + <value>The OpenID Relying Party's realm is not recognized as belonging to the OAuth Consumer identified by the consumer key given.</value> + </data> + <data name="RequestUrlMustNotHaveOAuthParameters" xml:space="preserve"> + <value>The request URL query MUST NOT contain any OAuth Protocol Parameters.</value> + </data> + <data name="SigningElementAlreadyAssociatedWithChannel" xml:space="preserve"> + <value>The signing element already has been associated with a channel.</value> + </data> + <data name="SigningElementsMustShareSameProtection" xml:space="preserve"> + <value>All signing elements must offer the same message protection.</value> + </data> + <data name="TokenNotFound" xml:space="preserve"> + <value>A token in the message was not recognized by the service provider.</value> + </data> + <data name="X509CertificateNotProvidedForSigning" xml:space="preserve"> + <value>The RSA-SHA1 signing binding element has not been set with a certificate for signing.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.sr.resx b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.sr.resx new file mode 100644 index 0000000..ef9ce60 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.sr.resx @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AccessTokenNotAuthorized" xml:space="preserve"> + <value>Ne može se poslati pristupni token za Consumera za token zahteva '{0}' pre nego što bude autorizovan.</value> + </data> + <data name="BadAccessTokenInProtectedResourceRequest" xml:space="preserve"> + <value>Pristupni token '{0}' je neispravan ili je mu je istekao rok važenja.</value> + </data> + <data name="ConsumerOrTokenSecretNotFound" xml:space="preserve"> + <value>Greška u traženju šifre za korisnika ili za token.</value> + </data> + <data name="IncorrectVerifier" xml:space="preserve"> + <value>oauth_verifier argument je neispravan.</value> + </data> + <data name="InvalidIncomingMessage" xml:space="preserve"> + <value>Neispravna OAuth poruka je primljena i odbačena.</value> + </data> + <data name="MessageNotAllowedExtraParameters" xml:space="preserve"> + <value>Poruka {0} je sadržala višak podataka koji nije dozvoljen.</value> + </data> + <data name="MinimumConsumerVersionRequirementNotMet" xml:space="preserve"> + <value>Ovaj provajder OAuth usluge zahteva od OAuth korisnika da da implementira OAuth {0}, ali izgleda da korisnika podržava jedino {1}.</value> + </data> + <data name="OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface" xml:space="preserve"> + <value>Korišćenje OpenID+OAuth ekstenzije zahteva da token menadžer koji se koristi implementira {0} interfejs.</value> + </data> + <data name="OpenIdOAuthRealmConsumerKeyDoNotMatch" xml:space="preserve"> + <value>OpenID Relying Party domen nije prepoznat kao da pripada OAuth Consumeru identifikovanom po datom korisničkom tokenu.</value> + </data> + <data name="RequestUrlMustNotHaveOAuthParameters" xml:space="preserve"> + <value>URL upit zahteva NE SME sadržati bilo kakve OAuth Protocol Parametre.</value> + </data> + <data name="SigningElementAlreadyAssociatedWithChannel" xml:space="preserve"> + <value>Potpisujući element je već asociran sa kanalom.</value> + </data> + <data name="SigningElementsMustShareSameProtection" xml:space="preserve"> + <value>Svi elementi za potpisivanje moraju nuditi istu zaštitu poruke.</value> + </data> + <data name="TokenNotFound" xml:space="preserve"> + <value>Token u poruci nije prepoznat od strane provajdera usluge.</value> + </data> + <data name="X509CertificateNotProvidedForSigning" xml:space="preserve"> + <value>RSA-SHA1 potpisujući povezujući element nije podešen sa sertifikatom za potpisivanje.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Protocol.cs b/src/DotNetOpenAuth.OAuth/OAuth/Protocol.cs new file mode 100644 index 0000000..4418b5e --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Protocol.cs @@ -0,0 +1,148 @@ +//----------------------------------------------------------------------- +// <copyright file="Protocol.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An enumeration of the OAuth protocol versions supported by this library. + /// </summary> + public enum ProtocolVersion { + /// <summary> + /// OAuth 1.0 specification + /// </summary> + V10, + + /// <summary> + /// OAuth 1.0a specification + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "a", Justification = "By design.")] + V10a, + } + + /// <summary> + /// Constants used in the OAuth protocol. + /// </summary> + /// <remarks> + /// OAuth Protocol Parameter names and values are case sensitive. Each OAuth Protocol Parameters MUST NOT appear more than once per request, and are REQUIRED unless otherwise noted, + /// per OAuth 1.0 section 5. + /// </remarks> + [DebuggerDisplay("OAuth {Version}")] + internal class Protocol { + /// <summary> + /// The namespace to use for V1.0 of the protocol. + /// </summary> + internal const string DataContractNamespaceV10 = "http://oauth.net/core/1.0/"; + + /// <summary> + /// The prefix used for all key names in the protocol. + /// </summary> + internal const string ParameterPrefix = "oauth_"; + + /// <summary> + /// The string representation of a <see cref="Version"/> instance to be used to represent OAuth 1.0a. + /// </summary> + internal const string V10aVersion = "1.0.1"; + + /// <summary> + /// The scheme to use in Authorization header message requests. + /// </summary> + internal const string AuthorizationHeaderScheme = "OAuth"; + + /// <summary> + /// Gets the <see cref="Protocol"/> instance with values initialized for V1.0 of the protocol. + /// </summary> + internal static readonly Protocol V10 = new Protocol { + dataContractNamespace = DataContractNamespaceV10, + Version = new Version(1, 0), + ProtocolVersion = ProtocolVersion.V10, + }; + + /// <summary> + /// Gets the <see cref="Protocol"/> instance with values initialized for V1.0a of the protocol. + /// </summary> + internal static readonly Protocol V10a = new Protocol { + dataContractNamespace = DataContractNamespaceV10, + Version = new Version(V10aVersion), + ProtocolVersion = ProtocolVersion.V10a, + }; + + /// <summary> + /// A list of all supported OAuth versions, in order starting from newest version. + /// </summary> + internal static readonly List<Protocol> AllVersions = new List<Protocol>() { V10a, V10 }; + + /// <summary> + /// The default (or most recent) supported version of the OAuth protocol. + /// </summary> + internal static readonly Protocol Default = AllVersions[0]; + + /// <summary> + /// The namespace to use for this version of the protocol. + /// </summary> + private string dataContractNamespace; + + /// <summary> + /// Initializes a new instance of the <see cref="Protocol"/> class. + /// </summary> + internal Protocol() { + this.PublishedVersion = "1.0"; + } + + /// <summary> + /// Gets the OAuth version this instance represents. + /// </summary> + internal Version Version { get; private set; } + + /// <summary> + /// Gets the version to declare on the wire. + /// </summary> + internal string PublishedVersion { get; private set; } + + /// <summary> + /// Gets the <see cref="ProtocolVersion"/> enum value for the <see cref="Protocol"/> instance. + /// </summary> + internal ProtocolVersion ProtocolVersion { get; private set; } + + /// <summary> + /// Gets the namespace to use for this version of the protocol. + /// </summary> + internal string DataContractNamespace { + get { return this.dataContractNamespace; } + } + + /// <summary> + /// Gets the OAuth Protocol instance to use for the given version. + /// </summary> + /// <param name="version">The OAuth version to get.</param> + /// <returns>A matching <see cref="Protocol"/> instance.</returns> + public static Protocol Lookup(ProtocolVersion version) { + switch (version) { + case ProtocolVersion.V10: return Protocol.V10; + case ProtocolVersion.V10a: return Protocol.V10a; + default: throw new ArgumentOutOfRangeException("version"); + } + } + + /// <summary> + /// Gets the OAuth Protocol instance to use for the given version. + /// </summary> + /// <param name="version">The OAuth version to get.</param> + /// <returns>A matching <see cref="Protocol"/> instance.</returns> + internal static Protocol Lookup(Version version) { + Contract.Requires<ArgumentNullException>(version != null); + Contract.Requires<ArgumentOutOfRangeException>(AllVersions.Any(p => p.Version == version)); + return AllVersions.First(p => p.Version == version); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/SecuritySettings.cs b/src/DotNetOpenAuth.OAuth/OAuth/SecuritySettings.cs new file mode 100644 index 0000000..3329f09 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/SecuritySettings.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="SecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + /// <summary> + /// Security settings that may be applicable to both consumers and service providers. + /// </summary> + public class SecuritySettings { + /// <summary> + /// Initializes a new instance of the <see cref="SecuritySettings"/> class. + /// </summary> + protected SecuritySettings() { + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ServiceProvider.cs b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProvider.cs new file mode 100644 index 0000000..ec9206e --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProvider.cs @@ -0,0 +1,576 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProvider.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Security.Principal; + using System.ServiceModel.Channels; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// A web application that allows access via OAuth. + /// </summary> + /// <remarks> + /// <para>The Service Provider’s documentation should include:</para> + /// <list> + /// <item>The URLs (Request URLs) the Consumer will use when making OAuth requests, and the HTTP methods (i.e. GET, POST, etc.) used in the Request Token URL and Access Token URL.</item> + /// <item>Signature methods supported by the Service Provider.</item> + /// <item>Any additional request parameters that the Service Provider requires in order to obtain a Token. Service Provider specific parameters MUST NOT begin with oauth_.</item> + /// </list> + /// </remarks> + public class ServiceProvider : IDisposable { + /// <summary> + /// The name of the key to use in the HttpApplication cache to store the + /// instance of <see cref="NonceMemoryStore"/> to use. + /// </summary> + private const string ApplicationStoreKey = "DotNetOpenAuth.OAuth.ServiceProvider.HttpApplicationStore"; + + /// <summary> + /// The length of the verifier code (in raw bytes before base64 encoding) to generate. + /// </summary> + private const int VerifierCodeLength = 5; + + /// <summary> + /// The field behind the <see cref="OAuthChannel"/> property. + /// </summary> + private OAuthChannel channel; + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior on the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + public ServiceProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager) + : this(serviceDescription, tokenManager, new OAuthServiceProviderMessageFactory(tokenManager)) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior on the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + /// <param name="messageTypeProvider">An object that can figure out what type of message is being received for deserialization.</param> + public ServiceProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, OAuthServiceProviderMessageFactory messageTypeProvider) + : this(serviceDescription, tokenManager, DotNetOpenAuthSection.Configuration.OAuth.ServiceProvider.ApplicationStore.CreateInstance(HttpApplicationStore), messageTypeProvider) { + Contract.Requires<ArgumentNullException>(serviceDescription != null); + Contract.Requires<ArgumentNullException>(tokenManager != null); + Contract.Requires<ArgumentNullException>(messageTypeProvider != null); + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior on the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + /// <param name="nonceStore">The nonce store.</param> + public ServiceProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, INonceStore nonceStore) + : this(serviceDescription, tokenManager, nonceStore, new OAuthServiceProviderMessageFactory(tokenManager)) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior on the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + /// <param name="nonceStore">The nonce store.</param> + /// <param name="messageTypeProvider">An object that can figure out what type of message is being received for deserialization.</param> + public ServiceProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, INonceStore nonceStore, OAuthServiceProviderMessageFactory messageTypeProvider) { + Contract.Requires<ArgumentNullException>(serviceDescription != null); + Contract.Requires<ArgumentNullException>(tokenManager != null); + Contract.Requires<ArgumentNullException>(nonceStore != null); + Contract.Requires<ArgumentNullException>(messageTypeProvider != null); + + var signingElement = serviceDescription.CreateTamperProtectionElement(); + this.ServiceDescription = serviceDescription; + this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.ServiceProvider.SecuritySettings.CreateSecuritySettings(); + this.OAuthChannel = new OAuthChannel(signingElement, nonceStore, tokenManager, this.SecuritySettings, messageTypeProvider); + this.TokenGenerator = new StandardTokenGenerator(); + + Reporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, nonceStore); + } + + /// <summary> + /// Gets the standard state storage mechanism that uses ASP.NET's + /// HttpApplication state dictionary to store associations and nonces. + /// </summary> + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static INonceStore HttpApplicationStore { + get { + Contract.Ensures(Contract.Result<INonceStore>() != null); + + HttpContext context = HttpContext.Current; + ErrorUtilities.VerifyOperation(context != null, Strings.StoreRequiredWhenNoHttpContextAvailable, typeof(INonceStore).Name); + var store = (INonceStore)context.Application[ApplicationStoreKey]; + if (store == null) { + context.Application.Lock(); + try { + if ((store = (INonceStore)context.Application[ApplicationStoreKey]) == null) { + context.Application[ApplicationStoreKey] = store = new NonceMemoryStore(StandardExpirationBindingElement.MaximumMessageAge); + } + } finally { + context.Application.UnLock(); + } + } + + return store; + } + } + + /// <summary> + /// Gets the description of this Service Provider. + /// </summary> + public ServiceProviderDescription ServiceDescription { get; private set; } + + /// <summary> + /// Gets or sets the generator responsible for generating new tokens and secrets. + /// </summary> + public ITokenGenerator TokenGenerator { get; set; } + + /// <summary> + /// Gets the persistence store for tokens and secrets. + /// </summary> + public IServiceProviderTokenManager TokenManager { + get { return (IServiceProviderTokenManager)this.OAuthChannel.TokenManager; } + } + + /// <summary> + /// Gets the channel to use for sending/receiving messages. + /// </summary> + public Channel Channel { + get { return this.OAuthChannel; } + } + + /// <summary> + /// Gets the security settings for this service provider. + /// </summary> + public ServiceProviderSecuritySettings SecuritySettings { get; private set; } + + /// <summary> + /// Gets or sets the channel to use for sending/receiving messages. + /// </summary> + internal OAuthChannel OAuthChannel { + get { + return this.channel; + } + + set { + Contract.Requires<ArgumentNullException>(value != null); + this.channel = value; + } + } + + /// <summary> + /// Creates a cryptographically strong random verification code. + /// </summary> + /// <param name="format">The desired format of the verification code.</param> + /// <param name="length">The length of the code. + /// When <paramref name="format"/> is <see cref="VerificationCodeFormat.IncludedInCallback"/>, + /// this is the length of the original byte array before base64 encoding rather than the actual + /// length of the final string.</param> + /// <returns>The verification code.</returns> + public static string CreateVerificationCode(VerificationCodeFormat format, int length) { + Contract.Requires<ArgumentOutOfRangeException>(length >= 0); + + switch (format) { + case VerificationCodeFormat.IncludedInCallback: + return MessagingUtilities.GetCryptoRandomDataAsBase64(length); + case VerificationCodeFormat.AlphaNumericNoLookAlikes: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.AlphaNumericNoLookAlikes); + case VerificationCodeFormat.AlphaUpper: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.UppercaseLetters); + case VerificationCodeFormat.AlphaLower: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.LowercaseLetters); + case VerificationCodeFormat.Numeric: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.Digits); + default: + throw new ArgumentOutOfRangeException("format"); + } + } + + /// <summary> + /// Reads any incoming OAuth message. + /// </summary> + /// <returns>The deserialized message.</returns> + /// <remarks> + /// Requires HttpContext.Current. + /// </remarks> + public IDirectedProtocolMessage ReadRequest() { + return this.Channel.ReadFromRequest(); + } + + /// <summary> + /// Reads any incoming OAuth message. + /// </summary> + /// <param name="request">The HTTP request to read the message from.</param> + /// <returns>The deserialized message.</returns> + public IDirectedProtocolMessage ReadRequest(HttpRequestInfo request) { + return this.Channel.ReadFromRequest(request); + } + + /// <summary> + /// Gets the incoming request for an unauthorized token, if any. + /// </summary> + /// <returns>The incoming request, or null if no OAuth message was attached.</returns> + /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> + /// <remarks> + /// Requires HttpContext.Current. + /// </remarks> + public UnauthorizedTokenRequest ReadTokenRequest() { + return this.ReadTokenRequest(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Reads a request for an unauthorized token from the incoming HTTP request. + /// </summary> + /// <param name="request">The HTTP request to read from.</param> + /// <returns>The incoming request, or null if no OAuth message was attached.</returns> + /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> + public UnauthorizedTokenRequest ReadTokenRequest(HttpRequestInfo request) { + UnauthorizedTokenRequest message; + if (this.Channel.TryReadFromRequest(request, out message)) { + ErrorUtilities.VerifyProtocol(message.Version >= Protocol.Lookup(this.SecuritySettings.MinimumRequiredOAuthVersion).Version, OAuthStrings.MinimumConsumerVersionRequirementNotMet, this.SecuritySettings.MinimumRequiredOAuthVersion, message.Version); + } + return message; + } + + /// <summary> + /// Prepares a message containing an unauthorized token for the Consumer to use in a + /// user agent redirect for subsequent authorization. + /// </summary> + /// <param name="request">The token request message the Consumer sent that the Service Provider is now responding to.</param> + /// <returns>The response message to send using the <see cref="Channel"/>, after optionally adding extra data to it.</returns> + public UnauthorizedTokenResponse PrepareUnauthorizedTokenMessage(UnauthorizedTokenRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + string token = this.TokenGenerator.GenerateRequestToken(request.ConsumerKey); + string secret = this.TokenGenerator.GenerateSecret(); + UnauthorizedTokenResponse response = new UnauthorizedTokenResponse(request, token, secret); + + return response; + } + + /// <summary> + /// Gets the incoming request for the Service Provider to authorize a Consumer's + /// access to some protected resources. + /// </summary> + /// <returns>The incoming request, or null if no OAuth message was attached.</returns> + /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> + /// <remarks> + /// Requires HttpContext.Current. + /// </remarks> + public UserAuthorizationRequest ReadAuthorizationRequest() { + return this.ReadAuthorizationRequest(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Reads in a Consumer's request for the Service Provider to obtain permission from + /// the user to authorize the Consumer's access of some protected resource(s). + /// </summary> + /// <param name="request">The HTTP request to read from.</param> + /// <returns>The incoming request, or null if no OAuth message was attached.</returns> + /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> + public UserAuthorizationRequest ReadAuthorizationRequest(HttpRequestInfo request) { + UserAuthorizationRequest message; + this.Channel.TryReadFromRequest(request, out message); + return message; + } + + /// <summary> + /// Gets the OAuth authorization request included with an OpenID authentication + /// request, if there is one. + /// </summary> + /// <param name="openIdRequest">The OpenID authentication request.</param> + /// <returns> + /// The scope of access the relying party is requesting, or null if no OAuth request + /// is present. + /// </returns> + /// <remarks> + /// <para>Call this method rather than simply extracting the OAuth extension + /// out from the authentication request directly to ensure that the additional + /// security measures that are required are taken.</para> + /// </remarks> + public AuthorizationRequest ReadAuthorizationRequest(IHostProcessedRequest openIdRequest) { + Contract.Requires<ArgumentNullException>(openIdRequest != null); + Contract.Requires<InvalidOperationException>(this.TokenManager is ICombinedOpenIdProviderTokenManager); + var openidTokenManager = this.TokenManager as ICombinedOpenIdProviderTokenManager; + ErrorUtilities.VerifyOperation(openidTokenManager != null, OAuthStrings.OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface, typeof(IOpenIdOAuthTokenManager).FullName); + + var authzRequest = openIdRequest.GetExtension<AuthorizationRequest>(); + if (authzRequest == null) { + return null; + } + + // OpenID+OAuth spec section 9: + // The Combined Provider SHOULD verify that the consumer key passed in the + // request is authorized to be used for the realm passed in the request. + string expectedConsumerKey = openidTokenManager.GetConsumerKey(openIdRequest.Realm); + ErrorUtilities.VerifyProtocol( + string.Equals(expectedConsumerKey, authzRequest.Consumer, StringComparison.Ordinal), + OAuthStrings.OpenIdOAuthRealmConsumerKeyDoNotMatch); + + return authzRequest; + } + + /// <summary> + /// Attaches the authorization response to an OpenID authentication response. + /// </summary> + /// <param name="openIdAuthenticationRequest">The OpenID authentication request.</param> + /// <param name="consumerKey">The consumer key. Must be <c>null</c> if and only if <paramref name="scope"/> is null.</param> + /// <param name="scope">The approved access scope. Use <c>null</c> to indicate no access was granted. The empty string will be interpreted as some default level of access is granted.</param> + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We want to take IAuthenticationRequest because that's the only supported use case.")] + [Obsolete("Call the overload that doesn't take a consumerKey instead.")] + public void AttachAuthorizationResponse(IHostProcessedRequest openIdAuthenticationRequest, string consumerKey, string scope) { + Contract.Requires<ArgumentNullException>(openIdAuthenticationRequest != null); + Contract.Requires<ArgumentException>((consumerKey == null) == (scope == null)); + Contract.Requires<InvalidOperationException>(this.TokenManager is ICombinedOpenIdProviderTokenManager); + var openidTokenManager = (ICombinedOpenIdProviderTokenManager)this.TokenManager; + ErrorUtilities.VerifyArgument(consumerKey == null || consumerKey == openidTokenManager.GetConsumerKey(openIdAuthenticationRequest.Realm), OAuthStrings.OpenIdOAuthRealmConsumerKeyDoNotMatch); + + this.AttachAuthorizationResponse(openIdAuthenticationRequest, scope); + } + + /// <summary> + /// Attaches the authorization response to an OpenID authentication response. + /// </summary> + /// <param name="openIdAuthenticationRequest">The OpenID authentication request.</param> + /// <param name="scope">The approved access scope. Use <c>null</c> to indicate no access was granted. The empty string will be interpreted as some default level of access is granted.</param> + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We want to take IAuthenticationRequest because that's the only supported use case.")] + public void AttachAuthorizationResponse(IHostProcessedRequest openIdAuthenticationRequest, string scope) { + Contract.Requires<ArgumentNullException>(openIdAuthenticationRequest != null); + Contract.Requires<InvalidOperationException>(this.TokenManager is ICombinedOpenIdProviderTokenManager); + + var openidTokenManager = this.TokenManager as ICombinedOpenIdProviderTokenManager; + IOpenIdMessageExtension response; + if (scope != null) { + // Generate an authorized request token to return to the relying party. + string consumerKey = openidTokenManager.GetConsumerKey(openIdAuthenticationRequest.Realm); + var approvedResponse = new AuthorizationApprovedResponse { + RequestToken = this.TokenGenerator.GenerateRequestToken(consumerKey), + Scope = scope, + }; + openidTokenManager.StoreOpenIdAuthorizedRequestToken(consumerKey, approvedResponse); + response = approvedResponse; + } else { + response = new AuthorizationDeclinedResponse(); + } + + openIdAuthenticationRequest.AddResponseExtension(response); + } + + /// <summary> + /// Prepares the message to send back to the consumer following proper authorization of + /// a token by an interactive user at the Service Provider's web site. + /// </summary> + /// <param name="request">The Consumer's original authorization request.</param> + /// <returns> + /// The message to send to the Consumer using <see cref="Channel"/> if one is necessary. + /// Null if the Consumer did not request a callback as part of the authorization request. + /// </returns> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] + public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + // It is very important for us to ignore the oauth_callback argument in the + // UserAuthorizationRequest if the Consumer is a 1.0a consumer or else we + // open up a security exploit. + IServiceProviderRequestToken token = this.TokenManager.GetRequestToken(request.RequestToken); + Uri callback; + if (request.Version >= Protocol.V10a.Version) { + // In OAuth 1.0a, we'll prefer the token-specific callback to the pre-registered one. + if (token.Callback != null) { + callback = token.Callback; + } else { + IConsumerDescription consumer = this.TokenManager.GetConsumer(token.ConsumerKey); + callback = consumer.Callback; + } + } else { + // In OAuth 1.0, we'll prefer the pre-registered callback over the token-specific one + // since 1.0 has a security weakness for user-modified callback URIs. + IConsumerDescription consumer = this.TokenManager.GetConsumer(token.ConsumerKey); + callback = consumer.Callback ?? request.Callback; + } + + return callback != null ? this.PrepareAuthorizationResponse(request, callback) : null; + } + + /// <summary> + /// Prepares the message to send back to the consumer following proper authorization of + /// a token by an interactive user at the Service Provider's web site. + /// </summary> + /// <param name="request">The Consumer's original authorization request.</param> + /// <param name="callback">The callback URI the consumer has previously registered + /// with this service provider or that came in the <see cref="UnauthorizedTokenRequest"/>.</param> + /// <returns> + /// The message to send to the Consumer using <see cref="Channel"/>. + /// </returns> + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] + public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request, Uri callback) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentNullException>(callback != null); + + var authorization = new UserAuthorizationResponse(callback, request.Version) { + RequestToken = request.RequestToken, + }; + + if (authorization.Version >= Protocol.V10a.Version) { + authorization.VerificationCode = CreateVerificationCode(VerificationCodeFormat.IncludedInCallback, VerifierCodeLength); + } + + return authorization; + } + + /// <summary> + /// Gets the incoming request to exchange an authorized token for an access token. + /// </summary> + /// <returns>The incoming request, or null if no OAuth message was attached.</returns> + /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> + /// <remarks> + /// Requires HttpContext.Current. + /// </remarks> + public AuthorizedTokenRequest ReadAccessTokenRequest() { + return this.ReadAccessTokenRequest(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Reads in a Consumer's request to exchange an authorized request token for an access token. + /// </summary> + /// <param name="request">The HTTP request to read from.</param> + /// <returns>The incoming request, or null if no OAuth message was attached.</returns> + /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> + public AuthorizedTokenRequest ReadAccessTokenRequest(HttpRequestInfo request) { + AuthorizedTokenRequest message; + this.Channel.TryReadFromRequest(request, out message); + return message; + } + + /// <summary> + /// Prepares and sends an access token to a Consumer, and invalidates the request token. + /// </summary> + /// <param name="request">The Consumer's message requesting an access token.</param> + /// <returns>The HTTP response to actually send to the Consumer.</returns> + public AuthorizedTokenResponse PrepareAccessTokenMessage(AuthorizedTokenRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + ErrorUtilities.VerifyProtocol(this.TokenManager.IsRequestTokenAuthorized(request.RequestToken), OAuthStrings.AccessTokenNotAuthorized, request.RequestToken); + + string accessToken = this.TokenGenerator.GenerateAccessToken(request.ConsumerKey); + string tokenSecret = this.TokenGenerator.GenerateSecret(); + this.TokenManager.ExpireRequestTokenAndStoreNewAccessToken(request.ConsumerKey, request.RequestToken, accessToken, tokenSecret); + var grantAccess = new AuthorizedTokenResponse(request) { + AccessToken = accessToken, + TokenSecret = tokenSecret, + }; + + return grantAccess; + } + + /// <summary> + /// Gets the authorization (access token) for accessing some protected resource. + /// </summary> + /// <returns>The authorization message sent by the Consumer, or null if no authorization message is attached.</returns> + /// <remarks> + /// This method verifies that the access token and token secret are valid. + /// It falls on the caller to verify that the access token is actually authorized + /// to access the resources being requested. + /// </remarks> + /// <exception cref="ProtocolException">Thrown if an unexpected message is attached to the request.</exception> + public AccessProtectedResourceRequest ReadProtectedResourceAuthorization() { + return this.ReadProtectedResourceAuthorization(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Gets the authorization (access token) for accessing some protected resource. + /// </summary> + /// <param name="request">HTTP details from an incoming WCF message.</param> + /// <param name="requestUri">The URI of the WCF service endpoint.</param> + /// <returns>The authorization message sent by the Consumer, or null if no authorization message is attached.</returns> + /// <remarks> + /// This method verifies that the access token and token secret are valid. + /// It falls on the caller to verify that the access token is actually authorized + /// to access the resources being requested. + /// </remarks> + /// <exception cref="ProtocolException">Thrown if an unexpected message is attached to the request.</exception> + public AccessProtectedResourceRequest ReadProtectedResourceAuthorization(HttpRequestMessageProperty request, Uri requestUri) { + return this.ReadProtectedResourceAuthorization(new HttpRequestInfo(request, requestUri)); + } + + /// <summary> + /// Gets the authorization (access token) for accessing some protected resource. + /// </summary> + /// <param name="request">The incoming HTTP request.</param> + /// <returns>The authorization message sent by the Consumer, or null if no authorization message is attached.</returns> + /// <remarks> + /// This method verifies that the access token and token secret are valid. + /// It falls on the caller to verify that the access token is actually authorized + /// to access the resources being requested. + /// </remarks> + /// <exception cref="ProtocolException">Thrown if an unexpected message is attached to the request.</exception> + public AccessProtectedResourceRequest ReadProtectedResourceAuthorization(HttpRequestInfo request) { + Contract.Requires<ArgumentNullException>(request != null); + + AccessProtectedResourceRequest accessMessage; + if (this.Channel.TryReadFromRequest<AccessProtectedResourceRequest>(request, out accessMessage)) { + if (this.TokenManager.GetTokenType(accessMessage.AccessToken) != TokenType.AccessToken) { + throw new ProtocolException( + string.Format( + CultureInfo.CurrentCulture, + OAuthStrings.BadAccessTokenInProtectedResourceRequest, + accessMessage.AccessToken)); + } + } + + return accessMessage; + } + + /// <summary> + /// Creates a security principal that may be used. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>The <see cref="IPrincipal"/> instance that can be used for access control of resources.</returns> + public OAuthPrincipal CreatePrincipal(AccessProtectedResourceRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + + IServiceProviderAccessToken accessToken = this.TokenManager.GetAccessToken(request.AccessToken); + return new OAuthPrincipal(accessToken); + } + + #region IDisposable Members + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + if (disposing) { + this.Channel.Dispose(); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderDescription.cs b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderDescription.cs new file mode 100644 index 0000000..6205f55 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderDescription.cs @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProviderDescription.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A description of the endpoints on a Service Provider. + /// </summary> + public class ServiceProviderDescription { + /// <summary> + /// The field used to store the value of the <see cref="RequestTokenEndpoint"/> property. + /// </summary> + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private MessageReceivingEndpoint requestTokenEndpoint; + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderDescription"/> class. + /// </summary> + public ServiceProviderDescription() { + this.ProtocolVersion = Protocol.Default.ProtocolVersion; + } + + /// <summary> + /// Gets or sets the OAuth version supported by the Service Provider. + /// </summary> + public ProtocolVersion ProtocolVersion { get; set; } + + /// <summary> + /// Gets or sets the URL used to obtain an unauthorized Request Token, + /// described in Section 6.1 (Obtaining an Unauthorized Request Token). + /// </summary> + /// <remarks> + /// The request URL query MUST NOT contain any OAuth Protocol Parameters. + /// This is the URL that <see cref="OAuth.Messages.UnauthorizedTokenRequest"/> messages are directed to. + /// </remarks> + /// <exception cref="ArgumentException">Thrown if this property is set to a URI with OAuth protocol parameters.</exception> + public MessageReceivingEndpoint RequestTokenEndpoint { + get { + return this.requestTokenEndpoint; + } + + set { + if (value != null && UriUtil.QueryStringContainPrefixedParameters(value.Location, OAuth.Protocol.ParameterPrefix)) { + throw new ArgumentException(OAuthStrings.RequestUrlMustNotHaveOAuthParameters); + } + + this.requestTokenEndpoint = value; + } + } + + /// <summary> + /// Gets or sets the URL used to obtain User authorization for Consumer access, + /// described in Section 6.2 (Obtaining User Authorization). + /// </summary> + /// <remarks> + /// This is the URL that <see cref="OAuth.Messages.UserAuthorizationRequest"/> messages are + /// indirectly (via the user agent) sent to. + /// </remarks> + public MessageReceivingEndpoint UserAuthorizationEndpoint { get; set; } + + /// <summary> + /// Gets or sets the URL used to exchange the User-authorized Request Token + /// for an Access Token, described in Section 6.3 (Obtaining an Access Token). + /// </summary> + /// <remarks> + /// This is the URL that <see cref="OAuth.Messages.AuthorizedTokenRequest"/> messages are directed to. + /// </remarks> + public MessageReceivingEndpoint AccessTokenEndpoint { get; set; } + + /// <summary> + /// Gets or sets the signing policies that apply to this Service Provider. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Type initializers require this format.")] + public ITamperProtectionChannelBindingElement[] TamperProtectionElements { get; set; } + + /// <summary> + /// Gets the OAuth version supported by the Service Provider. + /// </summary> + internal Version Version { + get { return Protocol.Lookup(this.ProtocolVersion).Version; } + } + + /// <summary> + /// Creates a signing element that includes all the signing elements this service provider supports. + /// </summary> + /// <returns>The created signing element.</returns> + internal ITamperProtectionChannelBindingElement CreateTamperProtectionElement() { + Contract.Requires(this.TamperProtectionElements != null); + return new SigningBindingElementChain(this.TamperProtectionElements.Select(el => (ITamperProtectionChannelBindingElement)el.Clone()).ToArray()); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderSecuritySettings.cs b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderSecuritySettings.cs new file mode 100644 index 0000000..701e36c --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderSecuritySettings.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProviderSecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + + /// <summary> + /// Security settings that are applicable to service providers. + /// </summary> + public class ServiceProviderSecuritySettings : SecuritySettings { + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderSecuritySettings"/> class. + /// </summary> + internal ServiceProviderSecuritySettings() { + } + + /// <summary> + /// Gets or sets the minimum required version of OAuth that must be implemented by a Consumer. + /// </summary> + public ProtocolVersion MinimumRequiredOAuthVersion { get; set; } + + /// <summary> + /// Gets or sets the maximum time a user can take to complete authorization. + /// </summary> + /// <remarks> + /// This time limit serves as a security mitigation against brute force attacks to + /// compromise (unauthorized or authorized) request tokens. + /// Longer time limits is more friendly to slow users or consumers, while shorter + /// time limits provide better security. + /// </remarks> + public TimeSpan MaximumRequestTokenTimeToLive { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/VerificationCodeFormat.cs b/src/DotNetOpenAuth.OAuth/OAuth/VerificationCodeFormat.cs new file mode 100644 index 0000000..a6560d8 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/VerificationCodeFormat.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------- +// <copyright file="VerificationCodeFormat.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System.Diagnostics.CodeAnalysis; + + /// <summary> + /// The different formats a user authorization verifier code can take + /// in order to be as secure as possible while being compatible with + /// the type of OAuth Consumer requesting access. + /// </summary> + /// <remarks> + /// Some Consumers may be set-top boxes, video games, mobile devies, etc. + /// with very limited character entry support and no ability to receive + /// a callback URI. OAuth 1.0a requires that these devices operators + /// must manually key in a verifier code, so in these cases it better + /// be possible to do so given the input options on that device. + /// </remarks> + public enum VerificationCodeFormat { + /// <summary> + /// The strongest verification code. + /// The best option for web consumers since a callback is usually an option. + /// </summary> + IncludedInCallback, + + /// <summary> + /// A combination of upper and lowercase letters and numbers may be used, + /// allowing a computer operator to easily read from the screen and key + /// in the verification code. + /// </summary> + /// <remarks> + /// Some letters and numbers will be skipped where they are visually similar + /// enough that they can be difficult to distinguish when displayed with most fonts. + /// </remarks> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Alikes", Justification = "Breaking change of existing API")] + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "AlphaNumeric", Justification = "Breaking change of existing API")] + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LookAlikes", Justification = "Breaking change of existing API")] + AlphaNumericNoLookAlikes, + + /// <summary> + /// Only uppercase letters will be used in the verification code. + /// Verification codes are case-sensitive, so consumers with fixed + /// keyboards with only one character case option may require this option. + /// </summary> + AlphaUpper, + + /// <summary> + /// Only lowercase letters will be used in the verification code. + /// Verification codes are case-sensitive, so consumers with fixed + /// keyboards with only one character case option may require this option. + /// </summary> + AlphaLower, + + /// <summary> + /// Only the numbers 0-9 will be used in the verification code. + /// Must useful for consumers running on mobile phone devices. + /// </summary> + Numeric, + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/WebConsumer.cs b/src/DotNetOpenAuth.OAuth/OAuth/WebConsumer.cs new file mode 100644 index 0000000..de37b80 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/WebConsumer.cs @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------- +// <copyright file="WebConsumer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A website or application that uses OAuth to access the Service Provider on behalf of the User. + /// </summary> + /// <remarks> + /// The methods on this class are thread-safe. Provided the properties are set and not changed + /// afterward, a single instance of this class may be used by an entire web application safely. + /// </remarks> + public class WebConsumer : ConsumerBase { + /// <summary> + /// Initializes a new instance of the <see cref="WebConsumer"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior of the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + public WebConsumer(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) + : base(serviceDescription, tokenManager) { + } + + /// <summary> + /// Begins an OAuth authorization request and redirects the user to the Service Provider + /// to provide that authorization. Upon successful authorization, the user is redirected + /// back to the current page. + /// </summary> + /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> + /// <remarks> + /// Requires HttpContext.Current. + /// </remarks> + public UserAuthorizationRequest PrepareRequestUserAuthorization() { + Uri callback = this.Channel.GetRequestFromContext().UrlBeforeRewriting.StripQueryArgumentsWithPrefix(Protocol.ParameterPrefix); + return this.PrepareRequestUserAuthorization(callback, null, null); + } + + /// <summary> + /// Prepares an OAuth message that begins an authorization request that will + /// redirect the user to the Service Provider to provide that authorization. + /// </summary> + /// <param name="callback"> + /// An optional Consumer URL that the Service Provider should redirect the + /// User Agent to upon successful authorization. + /// </param> + /// <param name="requestParameters">Extra parameters to add to the request token message. Optional.</param> + /// <param name="redirectParameters">Extra parameters to add to the redirect to Service Provider message. Optional.</param> + /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> + public UserAuthorizationRequest PrepareRequestUserAuthorization(Uri callback, IDictionary<string, string> requestParameters, IDictionary<string, string> redirectParameters) { + string token; + return this.PrepareRequestUserAuthorization(callback, requestParameters, redirectParameters, out token); + } + + /// <summary> + /// Processes an incoming authorization-granted message from an SP and obtains an access token. + /// </summary> + /// <returns>The access token, or null if no incoming authorization message was recognized.</returns> + /// <remarks> + /// Requires HttpContext.Current. + /// </remarks> + public AuthorizedTokenResponse ProcessUserAuthorization() { + return this.ProcessUserAuthorization(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Attaches an OAuth authorization request to an outgoing OpenID authentication request. + /// </summary> + /// <param name="openIdAuthenticationRequest">The OpenID authentication request.</param> + /// <param name="scope">The scope of access that is requested of the service provider.</param> + public void AttachAuthorizationRequest(IAuthenticationRequest openIdAuthenticationRequest, string scope) { + Contract.Requires<ArgumentNullException>(openIdAuthenticationRequest != null); + + var authorizationRequest = new AuthorizationRequest { + Consumer = this.ConsumerKey, + Scope = scope, + }; + + openIdAuthenticationRequest.AddExtension(authorizationRequest); + } + + /// <summary> + /// Processes an incoming authorization-granted message from an SP and obtains an access token. + /// </summary> + /// <param name="openIdAuthenticationResponse">The OpenID authentication response that may be carrying an authorized request token.</param> + /// <returns> + /// The access token, or null if OAuth authorization was denied by the user or service provider. + /// </returns> + /// <remarks> + /// The access token, if granted, is automatically stored in the <see cref="ConsumerBase.TokenManager"/>. + /// The token manager instance must implement <see cref="IOpenIdOAuthTokenManager"/>. + /// </remarks> + public AuthorizedTokenResponse ProcessUserAuthorization(IAuthenticationResponse openIdAuthenticationResponse) { + Contract.Requires<ArgumentNullException>(openIdAuthenticationResponse != null); + Contract.Requires<InvalidOperationException>(this.TokenManager is IOpenIdOAuthTokenManager); + var openidTokenManager = this.TokenManager as IOpenIdOAuthTokenManager; + ErrorUtilities.VerifyOperation(openidTokenManager != null, OAuthStrings.OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface, typeof(IOpenIdOAuthTokenManager).FullName); + + // The OAuth extension is only expected in positive assertion responses. + if (openIdAuthenticationResponse.Status != AuthenticationStatus.Authenticated) { + return null; + } + + // Retrieve the OAuth extension + var positiveAuthorization = openIdAuthenticationResponse.GetExtension<AuthorizationApprovedResponse>(); + if (positiveAuthorization == null) { + return null; + } + + // Prepare a message to exchange the request token for an access token. + // We are careful to use a v1.0 message version so that the oauth_verifier is not required. + var requestAccess = new AuthorizedTokenRequest(this.ServiceProvider.AccessTokenEndpoint, Protocol.V10.Version) { + RequestToken = positiveAuthorization.RequestToken, + ConsumerKey = this.ConsumerKey, + }; + + // Retrieve the access token and store it in the token manager. + openidTokenManager.StoreOpenIdAuthorizedRequestToken(this.ConsumerKey, positiveAuthorization); + var grantAccess = this.Channel.Request<AuthorizedTokenResponse>(requestAccess); + this.TokenManager.ExpireRequestTokenAndStoreNewAccessToken(this.ConsumerKey, positiveAuthorization.RequestToken, grantAccess.AccessToken, grantAccess.TokenSecret); + + // Provide the caller with the access token so it may be associated with the user + // that is logging in. + return grantAccess; + } + + /// <summary> + /// Processes an incoming authorization-granted message from an SP and obtains an access token. + /// </summary> + /// <param name="request">The incoming HTTP request.</param> + /// <returns>The access token, or null if no incoming authorization message was recognized.</returns> + public AuthorizedTokenResponse ProcessUserAuthorization(HttpRequestInfo request) { + Contract.Requires<ArgumentNullException>(request != null); + + UserAuthorizationResponse authorizationMessage; + if (this.Channel.TryReadFromRequest<UserAuthorizationResponse>(request, out authorizationMessage)) { + string requestToken = authorizationMessage.RequestToken; + string verifier = authorizationMessage.VerificationCode; + return this.ProcessUserAuthorization(requestToken, verifier); + } else { + return null; + } + } + } +} |