diff options
Diffstat (limited to 'src/DotNetOpenAuth.ApplicationBlock')
19 files changed, 1937 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/Acme.cs b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/Acme.cs new file mode 100644 index 0000000..eff6018 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/Acme.cs @@ -0,0 +1,80 @@ +//----------------------------------------------------------------------- +// <copyright file="Acme.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock.CustomExtensions { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A sample custom OpenID extension factory. + /// </summary> + /// <remarks> + /// OpenID extension factories must be registered with the library. This can be + /// done by calling <see cref="Acme.Register"/>, or by adding a snippet + /// such as the following to your web.config file: + /// <example> + /// <dotNetOpenAuth> + /// <openid> + /// <extensionFactories> + /// <add type="DotNetOpenAuth.ApplicationBlock.CustomExtensions.Acme, DotNetOpenAuth.ApplicationBlock" /> + /// </extensionFactories> + /// </openid> + /// </dotNetOpenAuth> + /// </example> + /// </remarks> + public class Acme : IOpenIdExtensionFactory { + internal const string CustomExtensionTypeUri = "testextension"; + internal static readonly Version Version = new Version(1, 0); + + public static void Register(OpenIdRelyingParty relyingParty) { + if (relyingParty == null) { + throw new ArgumentNullException("relyingParty"); + } + + relyingParty.ExtensionFactories.Add(new Acme()); + } + + public static void Register(OpenIdProvider provider) { + if (provider == null) { + throw new ArgumentNullException("provider"); + } + + provider.ExtensionFactories.Add(new Acme()); + } + + #region IOpenIdExtensionFactory Members + + /// <summary> + /// Creates a new instance of some extension based on the received extension parameters. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + /// <remarks> + /// This factory method need only initialize properties in the instantiated extension object + /// that are not bound using <see cref="MessagePartAttribute"/>. + /// </remarks> + public IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole) { + if (typeUri == CustomExtensionTypeUri) { + return isProviderRole ? (IOpenIdMessageExtension)new AcmeRequest() : new AcmeResponse(); + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/AcmeRequest.cs b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/AcmeRequest.cs new file mode 100644 index 0000000..58597b8 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/AcmeRequest.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="AcmeRequest.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock.CustomExtensions { + using System; + using System.Collections.Generic; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + public class AcmeRequest : IOpenIdMessageExtension { + private IDictionary<string, string> extraData = new Dictionary<string, string>(); + + [MessagePart] + public string FavoriteFlavor { get; set; } + + #region IOpenIdMessageExtension Members + + public string TypeUri { + get { return Acme.CustomExtensionTypeUri; } + } + + public IEnumerable<string> AdditionalSupportedTypeUris { + get { return Enumerable.Empty<string>(); } + } + + public bool IsSignedByRemoteParty { get; set; } + + #endregion + + #region IMessage Members + + public Version Version { + get { return Acme.Version; } + } + + public IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + public void EnsureValidMessage() { + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/AcmeResponse.cs b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/AcmeResponse.cs new file mode 100644 index 0000000..f021d84 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/AcmeResponse.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// <copyright file="AcmeResponse.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock.CustomExtensions { + using System; + using System.Collections.Generic; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + [Serializable] + public class AcmeResponse : IOpenIdMessageExtension { + private IDictionary<string, string> extraData = new Dictionary<string, string>(); + + [MessagePart] + public string FavoriteIceCream { get; set; } + + #region IOpenIdMessageExtension Members + + /// <summary> + /// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements. + /// </summary> + public string TypeUri { + get { return Acme.CustomExtensionTypeUri; } + } + + /// <summary> + /// Gets the additional TypeURIs that are supported by this extension, in preferred order. + /// May be empty if none other than <see cref="TypeUri"/> is supported, but + /// should not be null. + /// </summary> + /// <remarks> + /// Useful for reading in messages with an older version of an extension. + /// The value in the <see cref="TypeUri"/> property is always checked before + /// trying this list. + /// If you do support multiple versions of an extension using this method, + /// consider adding a CreateResponse method to your request extension class + /// so that the response can have the context it needs to remain compatible + /// given the version of the extension in the request message. + /// The <see cref="Extensions.SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example. + /// </remarks> + public IEnumerable<string> AdditionalSupportedTypeUris { + get { return Enumerable.Empty<string>(); } + } + + public bool IsSignedByRemoteParty { get; set; } + + #endregion + + #region IMessage Members + + public Version Version { + get { return Acme.Version; } + } + + public IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + public void EnsureValidMessage() { + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/UIRequestAtRelyingPartyFactory.cs b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/UIRequestAtRelyingPartyFactory.cs new file mode 100644 index 0000000..afa242e --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/CustomExtensions/UIRequestAtRelyingPartyFactory.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="UIRequestAtRelyingPartyFactory.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock.CustomExtensions { + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Extensions.UI; + + /// <summary> + /// An extension factory that allows the <see cref="UIRequest"/> extension to be received by the relying party. + /// </summary> + /// <remarks> + /// Typically UIRequest is only received by the Provider. But Google mirrors back this data to the relying party + /// if our web user is already logged into Google. + /// See the OpenIdRelyingPartyWebForms sample's DetectGoogleSession.aspx page for usage of this factory. + /// </remarks> + public class UIRequestAtRelyingPartyFactory : IOpenIdExtensionFactory { + /// <summary> + /// The Type URI for the UI extension. + /// </summary> + private const string UITypeUri = "http://specs.openid.net/extensions/ui/1.0"; + + /// <summary> + /// Allows UIRequest extensions to be received by the relying party. Useful when Google mirrors back the request + /// to indicate that a user is logged in. + /// </summary> + /// <param name="typeUri">The type URI of the extension.</param> + /// <param name="data">The parameters associated specifically with this extension.</param> + /// <param name="baseMessage">The OpenID message carrying this extension.</param> + /// <param name="isProviderRole">A value indicating whether this extension is being received at the OpenID Provider.</param> + /// <returns> + /// An instance of <see cref="IOpenIdMessageExtension"/> if the factory recognizes + /// the extension described in the input parameters; <c>null</c> otherwise. + /// </returns> + public DotNetOpenAuth.OpenId.Messages.IOpenIdMessageExtension Create(string typeUri, IDictionary<string, string> data, IProtocolMessageWithExtensions baseMessage, bool isProviderRole) { + if (typeUri == UITypeUri && !isProviderRole) { + return new UIRequest(); + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj b/src/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj new file mode 100644 index 0000000..b86c930 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj @@ -0,0 +1,191 @@ +<?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>
+ <ProductVersion>9.0.30729</ProductVersion>
+ <SchemaVersion>2.0</SchemaVersion>
+ <ProjectGuid>{AA78D112-D889-414B-A7D4-467B34C7B663}</ProjectGuid>
+ <OutputType>Library</OutputType>
+ <AppDesignerFolder>Properties</AppDesignerFolder>
+ <RootNamespace>DotNetOpenAuth.ApplicationBlock</RootNamespace>
+ <AssemblyName>DotNetOpenAuth.ApplicationBlock</AssemblyName>
+ <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <FileAlignment>512</FileAlignment>
+ <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>
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\</SolutionDir>
+ <RestorePackages>true</RestorePackages>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <DebugType>full</DebugType>
+ <Optimize>false</Optimize>
+ <OutputPath>bin\Debug\</OutputPath>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+ <DebugType>pdbonly</DebugType>
+ <Optimize>true</Optimize>
+ <OutputPath>bin\Release\</OutputPath>
+ <DefineConstants>TRACE</DefineConstants>
+ <ErrorReport>prompt</ErrorReport>
+ <WarningLevel>4</WarningLevel>
+ <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeAnalysis|AnyCPU' ">
+ <DebugSymbols>true</DebugSymbols>
+ <OutputPath>bin\CodeAnalysis\</OutputPath>
+ <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <DebugType>full</DebugType>
+ <PlatformTarget>AnyCPU</PlatformTarget>
+ <CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression>
+ <CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile>
+ <ErrorReport>prompt</ErrorReport>
+ <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
+ </PropertyGroup>
+ <PropertyGroup>
+ <DefineConstants>$(DefineConstants);SAMPLESONLY</DefineConstants>
+ </PropertyGroup>
+ <ItemGroup>
+ <Reference Include="DotNetOpenAuth.Core">
+ <HintPath>..\..\packages\DotNetOpenAuth.Core.4.0.0.12084\lib\net40-full\DotNetOpenAuth.Core.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.InfoCard">
+ <HintPath>..\..\packages\DotNetOpenAuth.InfoCard.4.0.0.12084\lib\net40-full\DotNetOpenAuth.InfoCard.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Core.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OAuth.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth.Common">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Common.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OAuth.Common.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth.Consumer">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.Consumer.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OAuth.Consumer.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth.ServiceProvider">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth.ServiceProvider.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OAuth.ServiceProvider.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth2">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth2.Core.0.23.0-draft2\lib\net40-full\DotNetOpenAuth.OAuth2.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth2.Client">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth2.Client.0.23.0-draft2\lib\net40-full\DotNetOpenAuth.OAuth2.Client.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OAuth2.Client.UI">
+ <HintPath>..\..\packages\DotNetOpenAuth.OAuth2.Client.UI.0.23.0-draft2\lib\net40-full\DotNetOpenAuth.OAuth2.Client.UI.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId">
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.Core.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OpenId.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId.Provider">
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.Provider.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OpenId.Provider.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenId.RelyingParty">
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenId.RelyingParty.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OpenId.RelyingParty.dll</HintPath>
+ </Reference>
+ <Reference Include="DotNetOpenAuth.OpenIdOAuth">
+ <HintPath>..\..\packages\DotNetOpenAuth.OpenIdOAuth.4.0.0.12084\lib\net40-full\DotNetOpenAuth.OpenIdOAuth.dll</HintPath>
+ </Reference>
+ <Reference Include="System" />
+ <Reference Include="System.configuration" />
+ <Reference Include="System.Core">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Runtime.Serialization" />
+ <Reference Include="System.ServiceModel.Web" />
+ <Reference Include="System.Web" />
+ <Reference Include="System.Xml.Linq">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data.DataSetExtensions">
+ <RequiredTargetFramework>3.5</RequiredTargetFramework>
+ </Reference>
+ <Reference Include="System.Data" />
+ <Reference Include="System.Xml" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="CustomExtensions\Acme.cs" />
+ <Compile Include="CustomExtensions\AcmeRequest.cs" />
+ <Compile Include="CustomExtensions\AcmeResponse.cs" />
+ <Compile Include="Facebook\FacebookClient.cs" />
+ <Compile Include="Facebook\FacebookGraph.cs" />
+ <Compile Include="CustomExtensions\UIRequestAtRelyingPartyFactory.cs" />
+ <Compile Include="GoogleConsumer.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="InMemoryClientAuthorizationTracker.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="InMemoryTokenManager.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Properties\AssemblyInfo.cs" />
+ <Compile Include="TokenManager.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="TwitterConsumer.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="Util.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="WindowsLiveClient.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="WindowsLiveGraph.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="YammerConsumer.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ <Compile Include="YubikeyRelyingParty.cs">
+ <SubType>Code</SubType>
+ </Compile>
+ </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>
+ <None Include="packages.config" />
+ </ItemGroup>
+ <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
+ <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))\EnlistmentInfo.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))' != '' " />
+ <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
+</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs b/src/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs new file mode 100644 index 0000000..4af1bbd --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------- +// <copyright file="FacebookClient.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2; + + public class FacebookClient : WebServerClient { + private static readonly AuthorizationServerDescription FacebookDescription = new AuthorizationServerDescription { + TokenEndpoint = new Uri("https://graph.facebook.com/oauth/access_token"), + AuthorizationEndpoint = new Uri("https://graph.facebook.com/oauth/authorize"), + }; + + /// <summary> + /// Initializes a new instance of the <see cref="FacebookClient"/> class. + /// </summary> + public FacebookClient() : base(FacebookDescription) { + this.AuthorizationTracker = new TokenManager(); + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs b/src/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs new file mode 100644 index 0000000..909ae9a --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------- +// <copyright file="FacebookGraph.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock.Facebook { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Runtime.Serialization; + using System.Runtime.Serialization.Json; + using System.Text; + + [DataContract] + public class FacebookGraph { + private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(FacebookGraph)); + + [DataMember(Name = "id")] + public long Id { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "first_name")] + public string FirstName { get; set; } + + [DataMember(Name = "last_name")] + public string LastName { get; set; } + + [DataMember(Name = "link")] + public Uri Link { get; set; } + + [DataMember(Name = "birthday")] + public string Birthday { get; set; } + + public static FacebookGraph Deserialize(string json) { + if (string.IsNullOrEmpty(json)) { + throw new ArgumentNullException("json"); + } + + return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json))); + } + + public static FacebookGraph Deserialize(Stream jsonStream) { + if (jsonStream == null) { + throw new ArgumentNullException("jsonStream"); + } + + return (FacebookGraph)jsonSerializer.ReadObject(jsonStream); + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs b/src/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs new file mode 100644 index 0000000..1bdb04d --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs @@ -0,0 +1,288 @@ +//----------------------------------------------------------------------- +// <copyright file="GoogleConsumer.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Text.RegularExpressions; + using System.Xml; + using System.Xml.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A consumer capable of communicating with Google Data APIs. + /// </summary> + public static class GoogleConsumer { + /// <summary> + /// The Consumer to use for accessing Google data APIs. + /// </summary> + public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription { + RequestTokenEndpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthAuthorizeToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetAccessToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> + /// A mapping between Google's applications and their URI scope values. + /// </summary> + private static readonly Dictionary<Applications, string> DataScopeUris = new Dictionary<Applications, string> { + { Applications.Analytics, "https://www.google.com/analytics/feeds/" }, + { Applications.GoogleBase, "http://www.google.com/base/feeds/" }, + { Applications.Blogger, "http://www.blogger.com/feeds" }, + { Applications.BookSearch, "http://www.google.com/books/feeds/" }, + { Applications.Calendar, "http://www.google.com/calendar/feeds/" }, + { Applications.Contacts, "http://www.google.com/m8/feeds/" }, + { Applications.DocumentsList, "http://docs.google.com/feeds/" }, + { Applications.Finance, "http://finance.google.com/finance/feeds/" }, + { Applications.Gmail, "https://mail.google.com/mail/feed/atom" }, + { Applications.Health, "https://www.google.com/h9/feeds/" }, + { Applications.Maps, "http://maps.google.com/maps/feeds/" }, + { Applications.OpenSocial, "http://sandbox.gmodules.com/api/" }, + { Applications.PicasaWeb, "http://picasaweb.google.com/data/" }, + { Applications.Spreadsheets, "http://spreadsheets.google.com/feeds/" }, + { Applications.WebmasterTools, "http://www.google.com/webmasters/tools/feeds/" }, + { Applications.YouTube, "http://gdata.youtube.com" }, + }; + + /// <summary> + /// The URI to get contacts once authorization is granted. + /// </summary> + private static readonly MessageReceivingEndpoint GetContactsEndpoint = new MessageReceivingEndpoint("http://www.google.com/m8/feeds/contacts/default/full/", HttpDeliveryMethods.GetRequest); + + /// <summary> + /// The many specific authorization scopes Google offers. + /// </summary> + [Flags] + public enum Applications : long { + /// <summary> + /// The Gmail address book. + /// </summary> + Contacts = 0x1, + + /// <summary> + /// Appointments in Google Calendar. + /// </summary> + Calendar = 0x2, + + /// <summary> + /// Blog post authoring. + /// </summary> + Blogger = 0x4, + + /// <summary> + /// Google Finance + /// </summary> + Finance = 0x8, + + /// <summary> + /// Google Gmail + /// </summary> + Gmail = 0x10, + + /// <summary> + /// Google Health + /// </summary> + Health = 0x20, + + /// <summary> + /// Google OpenSocial + /// </summary> + OpenSocial = 0x40, + + /// <summary> + /// Picasa Web + /// </summary> + PicasaWeb = 0x80, + + /// <summary> + /// Google Spreadsheets + /// </summary> + Spreadsheets = 0x100, + + /// <summary> + /// Webmaster Tools + /// </summary> + WebmasterTools = 0x200, + + /// <summary> + /// YouTube service + /// </summary> + YouTube = 0x400, + + /// <summary> + /// Google Docs + /// </summary> + DocumentsList = 0x800, + + /// <summary> + /// Google Book Search + /// </summary> + BookSearch = 0x1000, + + /// <summary> + /// Google Base + /// </summary> + GoogleBase = 0x2000, + + /// <summary> + /// Google Analytics + /// </summary> + Analytics = 0x4000, + + /// <summary> + /// Google Maps + /// </summary> + Maps = 0x8000, + } + + /// <summary> + /// The service description to use for accessing Google data APIs using an X509 certificate. + /// </summary> + /// <param name="signingCertificate">The signing certificate.</param> + /// <returns>A service description that can be used to create an instance of + /// <see cref="DesktopConsumer"/> or <see cref="WebConsumer"/>. </returns> + public static ServiceProviderDescription CreateRsaSha1ServiceDescription(X509Certificate2 signingCertificate) { + if (signingCertificate == null) { + throw new ArgumentNullException("signingCertificate"); + } + + return new ServiceProviderDescription { + RequestTokenEndpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetRequestToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthAuthorizeToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("https://www.google.com/accounts/OAuthGetAccessToken", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new RsaSha1ConsumerSigningBindingElement(signingCertificate) }, + }; + } + + /// <summary> + /// Requests authorization from Google to access data from a set of Google applications. + /// </summary> + /// <param name="consumer">The Google consumer previously constructed using <see cref="CreateWebConsumer"/> or <see cref="CreateDesktopConsumer"/>.</param> + /// <param name="requestedAccessScope">The requested access scope.</param> + public static void RequestAuthorization(WebConsumer consumer, Applications requestedAccessScope) { + if (consumer == null) { + throw new ArgumentNullException("consumer"); + } + + var extraParameters = new Dictionary<string, string> { + { "scope", GetScopeUri(requestedAccessScope) }, + }; + Uri callback = Util.GetCallbackUrlFromContext(); + var request = consumer.PrepareRequestUserAuthorization(callback, extraParameters, null); + consumer.Channel.Send(request); + } + + /// <summary> + /// Requests authorization from Google to access data from a set of Google applications. + /// </summary> + /// <param name="consumer">The Google consumer previously constructed using <see cref="CreateWebConsumer"/> or <see cref="CreateDesktopConsumer"/>.</param> + /// <param name="requestedAccessScope">The requested access scope.</param> + /// <param name="requestToken">The unauthorized request token assigned by Google.</param> + /// <returns>The request token</returns> + public static Uri RequestAuthorization(DesktopConsumer consumer, Applications requestedAccessScope, out string requestToken) { + if (consumer == null) { + throw new ArgumentNullException("consumer"); + } + + var extraParameters = new Dictionary<string, string> { + { "scope", GetScopeUri(requestedAccessScope) }, + }; + + return consumer.RequestUserAuthorization(extraParameters, null, out requestToken); + } + + /// <summary> + /// Gets the Gmail address book's contents. + /// </summary> + /// <param name="consumer">The Google consumer.</param> + /// <param name="accessToken">The access token previously retrieved.</param> + /// <param name="maxResults">The maximum number of entries to return. If you want to receive all of the contacts, rather than only the default maximum, you can specify a very large number here.</param> + /// <param name="startIndex">The 1-based index of the first result to be retrieved (for paging).</param> + /// <returns>An XML document returned by Google.</returns> + public static XDocument GetContacts(ConsumerBase consumer, string accessToken, int maxResults/* = 25*/, int startIndex/* = 1*/) { + if (consumer == null) { + throw new ArgumentNullException("consumer"); + } + + var extraData = new Dictionary<string, string>() { + { "start-index", startIndex.ToString(CultureInfo.InvariantCulture) }, + { "max-results", maxResults.ToString(CultureInfo.InvariantCulture) }, + }; + var request = consumer.PrepareAuthorizedRequest(GetContactsEndpoint, accessToken, extraData); + + // Enable gzip compression. Google only compresses the response for recognized user agent headers. - Mike Lim + request.AutomaticDecompression = DecompressionMethods.GZip; + request.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.151 Safari/534.16"; + + var response = consumer.Channel.WebRequestHandler.GetResponse(request); + string body = response.GetResponseReader().ReadToEnd(); + XDocument result = XDocument.Parse(body); + return result; + } + + public static void PostBlogEntry(ConsumerBase consumer, string accessToken, string blogUrl, string title, XElement body) { + string feedUrl; + var getBlogHome = WebRequest.Create(blogUrl); + using (var blogHomeResponse = getBlogHome.GetResponse()) { + using (StreamReader sr = new StreamReader(blogHomeResponse.GetResponseStream())) { + string homePageHtml = sr.ReadToEnd(); + Match m = Regex.Match(homePageHtml, @"http://www.blogger.com/feeds/\d+/posts/default"); + Debug.Assert(m.Success, "Posting operation failed."); + feedUrl = m.Value; + } + } + const string Atom = "http://www.w3.org/2005/Atom"; + XElement entry = new XElement( + XName.Get("entry", Atom), + new XElement(XName.Get("title", Atom), new XAttribute("type", "text"), title), + new XElement(XName.Get("content", Atom), new XAttribute("type", "xhtml"), body), + new XElement(XName.Get("category", Atom), new XAttribute("scheme", "http://www.blogger.com/atom/ns#"), new XAttribute("term", "oauthdemo"))); + + MemoryStream ms = new MemoryStream(); + XmlWriterSettings xws = new XmlWriterSettings() { + Encoding = Encoding.UTF8, + }; + XmlWriter xw = XmlWriter.Create(ms, xws); + entry.WriteTo(xw); + xw.Flush(); + + WebRequest request = consumer.PrepareAuthorizedRequest(new MessageReceivingEndpoint(feedUrl, HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), accessToken); + request.ContentType = "application/atom+xml"; + request.Method = "POST"; + request.ContentLength = ms.Length; + ms.Seek(0, SeekOrigin.Begin); + using (Stream requestStream = request.GetRequestStream()) { + ms.CopyTo(requestStream); + } + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { + if (response.StatusCode == HttpStatusCode.Created) { + // Success + } else { + // Error! + } + } + } + + /// <summary> + /// Gets the scope URI in Google's format. + /// </summary> + /// <param name="scope">The scope, which may include one or several Google applications.</param> + /// <returns>A space-delimited list of URIs for the requested Google applications.</returns> + public static string GetScopeUri(Applications scope) { + return string.Join(" ", Util.GetIndividualFlags(scope).Select(app => DataScopeUris[(Applications)app]).ToArray()); + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/InMemoryClientAuthorizationTracker.cs b/src/DotNetOpenAuth.ApplicationBlock/InMemoryClientAuthorizationTracker.cs new file mode 100644 index 0000000..0dad973 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/InMemoryClientAuthorizationTracker.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="InMemoryClientAuthorizationTracker.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.ServiceModel; + using System.Text; + using System.Threading; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2; + + internal class InMemoryClientAuthorizationTracker : IClientAuthorizationTracker { + private readonly Dictionary<int, IAuthorizationState> savedStates = new Dictionary<int, IAuthorizationState>(); + private int stateCounter; + + #region Implementation of IClientTokenManager + + /// <summary> + /// Gets the state of the authorization for a given callback URL and client state. + /// </summary> + /// <param name="callbackUrl">The callback URL.</param> + /// <param name="clientState">State of the client stored at the beginning of an authorization request.</param> + /// <returns>The authorization state; may be <c>null</c> if no authorization state matches.</returns> + public IAuthorizationState GetAuthorizationState(Uri callbackUrl, string clientState) { + IAuthorizationState state; + if (this.savedStates.TryGetValue(int.Parse(clientState), out state)) { + if (state.Callback != callbackUrl) { + throw new DotNetOpenAuth.Messaging.ProtocolException("Client state and callback URL do not match."); + } + } + + return state; + } + + #endregion + + internal IAuthorizationState NewAuthorization(HashSet<string> scope, out string clientState) { + int counter = Interlocked.Increment(ref this.stateCounter); + clientState = counter.ToString(CultureInfo.InvariantCulture); + return this.savedStates[counter] = new AuthorizationState(scope); + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs b/src/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs new file mode 100644 index 0000000..2a1e155 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs @@ -0,0 +1,145 @@ +//----------------------------------------------------------------------- +// <copyright file="InMemoryTokenManager.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Diagnostics; + + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + + /// <summary> + /// A token manager that only retains tokens in memory. + /// Meant for SHORT TERM USE TOKENS ONLY. + /// </summary> + /// <remarks> + /// A likely application of this class is for "Sign In With Twitter", + /// where the user only signs in without providing any authorization to access + /// Twitter APIs except to authenticate, since that access token is only useful once. + /// </remarks> + public class InMemoryTokenManager : IConsumerTokenManager, IOpenIdOAuthTokenManager { + private Dictionary<string, string> tokensAndSecrets = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="InMemoryTokenManager"/> class. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + public InMemoryTokenManager(string consumerKey, string consumerSecret) { + if (string.IsNullOrEmpty(consumerKey)) { + throw new ArgumentNullException("consumerKey"); + } + + this.ConsumerKey = consumerKey; + this.ConsumerSecret = consumerSecret; + } + + /// <summary> + /// Gets the consumer key. + /// </summary> + /// <value>The consumer key.</value> + public string ConsumerKey { get; private set; } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + /// <value>The consumer secret.</value> + public string ConsumerSecret { get; private set; } + + #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> + public string GetTokenSecret(string token) { + return this.tokensAndSecrets[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> + public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) { + this.tokensAndSecrets[response.Token] = response.TokenSecret; + } + + /// <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> + public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) { + this.tokensAndSecrets.Remove(requestToken); + this.tokensAndSecrets[accessToken] = 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> + public TokenType GetTokenType(string token) { + throw new NotImplementedException(); + } + + #endregion + + #region IOpenIdOAuthTokenManager Members + + /// <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> + public void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization) { + this.tokensAndSecrets[authorization.RequestToken] = string.Empty; + } + + #endregion + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.ApplicationBlock/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.ApplicationBlock/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0384014 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DotNetOpenAuth.ApplicationBlock")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("DotNetOpenAuth.ApplicationBlock")] +[assembly: AssemblyCopyright("Copyright © 2011 Outercurve Foundation")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6db0c62e-2f06-46fa-acfe-4d737467c8c7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/DotNetOpenAuth.ApplicationBlock/TokenManager.cs b/src/DotNetOpenAuth.ApplicationBlock/TokenManager.cs new file mode 100644 index 0000000..d1254af --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/TokenManager.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="TokenManager.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using DotNetOpenAuth.OAuth2; + + public class TokenManager : IClientAuthorizationTracker { + public IAuthorizationState GetAuthorizationState(Uri callbackUrl, string clientState) { + return new AuthorizationState { + Callback = callbackUrl, + }; + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs b/src/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs new file mode 100644 index 0000000..013e66b --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs @@ -0,0 +1,224 @@ +//----------------------------------------------------------------------- +// <copyright file="TwitterConsumer.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Globalization; + using System.IO; + using System.Net; + using System.Web; + using System.Xml; + using System.Xml.Linq; + using System.Xml.XPath; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A consumer capable of communicating with Twitter. + /// </summary> + public static class TwitterConsumer { + /// <summary> + /// The description of Twitter's OAuth protocol URIs for use with actually reading/writing + /// a user's private Twitter data. + /// </summary> + public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription { + RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> + /// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature. + /// </summary> + public static readonly ServiceProviderDescription SignInWithTwitterServiceDescription = new ServiceProviderDescription { + RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> + /// The URI to get a user's favorites. + /// </summary> + private static readonly MessageReceivingEndpoint GetFavoritesEndpoint = new MessageReceivingEndpoint("http://twitter.com/favorites.xml", HttpDeliveryMethods.GetRequest); + + /// <summary> + /// The URI to get the data on the user's home page. + /// </summary> + private static readonly MessageReceivingEndpoint GetFriendTimelineStatusEndpoint = new MessageReceivingEndpoint("http://twitter.com/statuses/friends_timeline.xml", HttpDeliveryMethods.GetRequest); + + private static readonly MessageReceivingEndpoint UpdateProfileBackgroundImageEndpoint = new MessageReceivingEndpoint("http://twitter.com/account/update_profile_background_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); + + private static readonly MessageReceivingEndpoint UpdateProfileImageEndpoint = new MessageReceivingEndpoint("http://twitter.com/account/update_profile_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); + + private static readonly MessageReceivingEndpoint VerifyCredentialsEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/verify_credentials.xml", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); + + /// <summary> + /// The consumer used for the Sign in to Twitter feature. + /// </summary> + private static WebConsumer signInConsumer; + + /// <summary> + /// The lock acquired to initialize the <see cref="signInConsumer"/> field. + /// </summary> + private static object signInConsumerInitLock = new object(); + + /// <summary> + /// Initializes static members of the <see cref="TwitterConsumer"/> class. + /// </summary> + static TwitterConsumer() { + // Twitter can't handle the Expect 100 Continue HTTP header. + ServicePointManager.FindServicePoint(GetFavoritesEndpoint.Location).Expect100Continue = false; + } + + /// <summary> + /// Gets a value indicating whether the Twitter consumer key and secret are set in the web.config file. + /// </summary> + public static bool IsTwitterConsumerConfigured { + get { + return !string.IsNullOrEmpty(ConfigurationManager.AppSettings["twitterConsumerKey"]) && + !string.IsNullOrEmpty(ConfigurationManager.AppSettings["twitterConsumerSecret"]); + } + } + + /// <summary> + /// Gets the consumer to use for the Sign in to Twitter feature. + /// </summary> + /// <value>The twitter sign in.</value> + private static WebConsumer TwitterSignIn { + get { + if (signInConsumer == null) { + lock (signInConsumerInitLock) { + if (signInConsumer == null) { + signInConsumer = new WebConsumer(SignInWithTwitterServiceDescription, ShortTermUserSessionTokenManager); + } + } + } + + return signInConsumer; + } + } + + private static InMemoryTokenManager ShortTermUserSessionTokenManager { + get { + var store = HttpContext.Current.Session; + var tokenManager = (InMemoryTokenManager)store["TwitterShortTermUserSessionTokenManager"]; + if (tokenManager == null) { + string consumerKey = ConfigurationManager.AppSettings["twitterConsumerKey"]; + string consumerSecret = ConfigurationManager.AppSettings["twitterConsumerSecret"]; + if (IsTwitterConsumerConfigured) { + tokenManager = new InMemoryTokenManager(consumerKey, consumerSecret); + store["TwitterShortTermUserSessionTokenManager"] = tokenManager; + } else { + throw new InvalidOperationException("No Twitter OAuth consumer key and secret could be found in web.config AppSettings."); + } + } + + return tokenManager; + } + } + + public static XDocument GetUpdates(ConsumerBase twitter, string accessToken) { + IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFriendTimelineStatusEndpoint, accessToken); + return XDocument.Load(XmlReader.Create(response.GetResponseReader())); + } + + public static XDocument GetFavorites(ConsumerBase twitter, string accessToken) { + IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFavoritesEndpoint, accessToken); + return XDocument.Load(XmlReader.Create(response.GetResponseReader())); + } + + public static XDocument UpdateProfileBackgroundImage(ConsumerBase twitter, string accessToken, string image, bool tile) { + var parts = new[] { + MultipartPostPart.CreateFormFilePart("image", image, "image/" + Path.GetExtension(image).Substring(1).ToLowerInvariant()), + MultipartPostPart.CreateFormPart("tile", tile.ToString().ToLowerInvariant()), + }; + HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileBackgroundImageEndpoint, accessToken, parts); + request.ServicePoint.Expect100Continue = false; + IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request); + string responseString = response.GetResponseReader().ReadToEnd(); + return XDocument.Parse(responseString); + } + + public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, string pathToImage) { + string contentType = "image/" + Path.GetExtension(pathToImage).Substring(1).ToLowerInvariant(); + return UpdateProfileImage(twitter, accessToken, File.OpenRead(pathToImage), contentType); + } + + public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, Stream image, string contentType) { + var parts = new[] { + MultipartPostPart.CreateFormFilePart("image", "twitterPhoto", contentType, image), + }; + HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileImageEndpoint, accessToken, parts); + IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request); + string responseString = response.GetResponseReader().ReadToEnd(); + return XDocument.Parse(responseString); + } + + public static XDocument VerifyCredentials(ConsumerBase twitter, string accessToken) { + IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(VerifyCredentialsEndpoint, accessToken); + return XDocument.Load(XmlReader.Create(response.GetResponseReader())); + } + + public static string GetUsername(ConsumerBase twitter, string accessToken) { + XDocument xml = VerifyCredentials(twitter, accessToken); + XPathNavigator nav = xml.CreateNavigator(); + return nav.SelectSingleNode("/user/screen_name").Value; + } + + /// <summary> + /// Prepares a redirect that will send the user to Twitter to sign in. + /// </summary> + /// <param name="forceNewLogin">if set to <c>true</c> the user will be required to re-enter their Twitter credentials even if already logged in to Twitter.</param> + /// <returns>The redirect message.</returns> + /// <remarks> + /// Call <see cref="OutgoingWebResponse.Send"/> or + /// <c>return StartSignInWithTwitter().<see cref="MessagingUtilities.AsActionResult">AsActionResult()</see></c> + /// to actually perform the redirect. + /// </remarks> + public static OutgoingWebResponse StartSignInWithTwitter(bool forceNewLogin) { + var redirectParameters = new Dictionary<string, string>(); + if (forceNewLogin) { + redirectParameters["force_login"] = "true"; + } + Uri callback = MessagingUtilities.GetRequestUrlFromContext().StripQueryArgumentsWithPrefix("oauth_"); + var request = TwitterSignIn.PrepareRequestUserAuthorization(callback, null, redirectParameters); + return TwitterSignIn.Channel.PrepareResponse(request); + } + + /// <summary> + /// Checks the incoming web request to see if it carries a Twitter authentication response, + /// and provides the user's Twitter screen name and unique id if available. + /// </summary> + /// <param name="screenName">The user's Twitter screen name.</param> + /// <param name="userId">The user's Twitter unique user ID.</param> + /// <returns> + /// A value indicating whether Twitter authentication was successful; + /// otherwise <c>false</c> to indicate that no Twitter response was present. + /// </returns> + public static bool TryFinishSignInWithTwitter(out string screenName, out int userId) { + screenName = null; + userId = 0; + var response = TwitterSignIn.ProcessUserAuthorization(); + if (response == null) { + return false; + } + + screenName = response.ExtraData["screen_name"]; + userId = int.Parse(response.ExtraData["user_id"]); + + // If we were going to make this LOOK like OpenID even though it isn't, + // this seems like a reasonable, secure claimed id to allow the user to assume. + OpenId.Identifier fake_claimed_id = string.Format(CultureInfo.InvariantCulture, "http://twitter.com/{0}#{1}", screenName, userId); + + return true; + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/Util.cs b/src/DotNetOpenAuth.ApplicationBlock/Util.cs new file mode 100644 index 0000000..0bec372 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/Util.cs @@ -0,0 +1,259 @@ +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Net; + using DotNetOpenAuth.Messaging; + + public static class Util { + /// <summary> + /// Pseudo-random data generator. + /// </summary> + internal static readonly Random NonCryptoRandomDataGenerator = new Random(); + + /// <summary> + /// Sets the channel's outgoing HTTP requests to use default network credentials. + /// </summary> + /// <param name="channel">The channel to modify.</param> + public static void UseDefaultNetworkCredentialsOnOutgoingHttpRequests(this Channel channel) + { + Debug.Assert(!(channel.WebRequestHandler is WrappingWebRequestHandler), "Wrapping an already wrapped web request handler. This is legal, but highly suspect of a bug as you don't want to wrap the same channel repeatedly to apply the same effect."); + AddOutgoingHttpRequestTransform(channel, http => http.Credentials = CredentialCache.DefaultNetworkCredentials); + } + + /// <summary> + /// Adds some action to any outgoing HTTP request on this channel. + /// </summary> + /// <param name="channel">The channel's whose outgoing HTTP requests should be modified.</param> + /// <param name="action">The action to perform on outgoing HTTP requests.</param> + internal static void AddOutgoingHttpRequestTransform(this Channel channel, Action<HttpWebRequest> action) { + if (channel == null) { + throw new ArgumentNullException("channel"); + } + + if (action == null) { + throw new ArgumentNullException("action"); + } + + channel.WebRequestHandler = new WrappingWebRequestHandler(channel.WebRequestHandler, action); + } + + /// <summary> + /// Enumerates through the individual set bits in a flag enum. + /// </summary> + /// <param name="flags">The flags enum value.</param> + /// <returns>An enumeration of just the <i>set</i> bits in the flags enum.</returns> + internal static IEnumerable<long> GetIndividualFlags(Enum flags) { + long flagsLong = Convert.ToInt64(flags); + for (int i = 0; i < sizeof(long) * 8; i++) { // long is the type behind the largest enum + // Select an individual application from the scopes. + long individualFlagPosition = (long)Math.Pow(2, i); + long individualFlag = flagsLong & individualFlagPosition; + if (individualFlag == individualFlagPosition) { + yield return individualFlag; + } + } + } + + internal static Uri GetCallbackUrlFromContext() { + Uri callback = MessagingUtilities.GetRequestUrlFromContext().StripQueryArgumentsWithPrefix("oauth_"); + return callback; + } + + /// <summary> + /// Copies the contents of one stream to another. + /// </summary> + /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param> + /// <param name="copyTo">The stream to copy to, at the position where bytes should be written.</param> + /// <returns>The total number of bytes copied.</returns> + /// <remarks> + /// Copying begins at the streams' current positions. + /// The positions are NOT reset after copying is complete. + /// </remarks> + internal static int CopyTo(this Stream copyFrom, Stream copyTo) { + return CopyTo(copyFrom, copyTo, int.MaxValue); + } + + /// <summary> + /// Copies the contents of one stream to another. + /// </summary> + /// <param name="copyFrom">The stream to copy from, at the position where copying should begin.</param> + /// <param name="copyTo">The stream to copy to, at the position where bytes should be written.</param> + /// <param name="maximumBytesToCopy">The maximum bytes to copy.</param> + /// <returns>The total number of bytes copied.</returns> + /// <remarks> + /// Copying begins at the streams' current positions. + /// The positions are NOT reset after copying is complete. + /// </remarks> + internal static int CopyTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy) { + if (copyFrom == null) { + throw new ArgumentNullException("copyFrom"); + } + if (copyTo == null) { + throw new ArgumentNullException("copyTo"); + } + + byte[] buffer = new byte[1024]; + int readBytes; + int totalCopiedBytes = 0; + while ((readBytes = copyFrom.Read(buffer, 0, Math.Min(1024, maximumBytesToCopy))) > 0) { + int writeBytes = Math.Min(maximumBytesToCopy, readBytes); + copyTo.Write(buffer, 0, writeBytes); + totalCopiedBytes += writeBytes; + maximumBytesToCopy -= writeBytes; + } + + return totalCopiedBytes; + } + + /// <summary> + /// Wraps some instance of a web request handler in order to perform some extra operation on all + /// outgoing HTTP requests. + /// </summary> + private class WrappingWebRequestHandler : IDirectWebRequestHandler + { + /// <summary> + /// The handler being wrapped. + /// </summary> + private readonly IDirectWebRequestHandler wrappedHandler; + + /// <summary> + /// The action to perform on outgoing HTTP requests. + /// </summary> + private readonly Action<HttpWebRequest> action; + + /// <summary> + /// Initializes a new instance of the <see cref="WrappingWebRequestHandler"/> class. + /// </summary> + /// <param name="wrappedHandler">The HTTP handler to wrap.</param> + /// <param name="action">The action to perform on outgoing HTTP requests.</param> + internal WrappingWebRequestHandler(IDirectWebRequestHandler wrappedHandler, Action<HttpWebRequest> action) + { + if (wrappedHandler == null) { + throw new ArgumentNullException("wrappedHandler"); + } + + if (action == null) { + throw new ArgumentNullException("action"); + } + + this.wrappedHandler = wrappedHandler; + this.action = action; + } + + #region Implementation of IDirectWebRequestHandler + + /// <summary> + /// Determines whether this instance can support the specified options. + /// </summary> + /// <param name="options">The set of options that might be given in a subsequent web request.</param> + /// <returns> + /// <c>true</c> if this instance can support the specified options; otherwise, <c>false</c>. + /// </returns> + public bool CanSupport(DirectWebRequestOptions options) + { + return this.wrappedHandler.CanSupport(options); + } + + /// <summary> + /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity. + /// </summary> + /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param> + /// <returns> + /// The stream the caller should write out the entity data to. + /// </returns> + /// <exception cref="ProtocolException">Thrown for any network error.</exception> + /// <remarks> + /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/> + /// and any other appropriate properties <i>before</i> calling this method. + /// Callers <i>must</i> close and dispose of the request stream when they are done + /// writing to it to avoid taking up the connection too long and causing long waits on + /// subsequent requests.</para> + /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a + /// <see cref="ProtocolException"/> to abstract away the transport and provide + /// a single exception type for hosts to catch.</para> + /// </remarks> + public Stream GetRequestStream(HttpWebRequest request) + { + this.action(request); + return this.wrappedHandler.GetRequestStream(request); + } + + /// <summary> + /// Prepares an <see cref="HttpWebRequest"/> that contains an POST entity for sending the entity. + /// </summary> + /// <param name="request">The <see cref="HttpWebRequest"/> that should contain the entity.</param> + /// <param name="options">The options to apply to this web request.</param> + /// <returns> + /// The stream the caller should write out the entity data to. + /// </returns> + /// <exception cref="ProtocolException">Thrown for any network error.</exception> + /// <remarks> + /// <para>The caller should have set the <see cref="HttpWebRequest.ContentLength"/> + /// and any other appropriate properties <i>before</i> calling this method. + /// Callers <i>must</i> close and dispose of the request stream when they are done + /// writing to it to avoid taking up the connection too long and causing long waits on + /// subsequent requests.</para> + /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a + /// <see cref="ProtocolException"/> to abstract away the transport and provide + /// a single exception type for hosts to catch.</para> + /// </remarks> + public Stream GetRequestStream(HttpWebRequest request, DirectWebRequestOptions options) + { + this.action(request); + return this.wrappedHandler.GetRequestStream(request, options); + } + + /// <summary> + /// Processes an <see cref="HttpWebRequest"/> and converts the + /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance. + /// </summary> + /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param> + /// <returns>An instance of <see cref="IncomingWebResponse"/> describing the response.</returns> + /// <exception cref="ProtocolException">Thrown for any network error.</exception> + /// <remarks> + /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a + /// <see cref="ProtocolException"/> to abstract away the transport and provide + /// a single exception type for hosts to catch. The <see cref="WebException.Response"/> + /// value, if set, should be Closed before throwing.</para> + /// </remarks> + public IncomingWebResponse GetResponse(HttpWebRequest request) + { + // If the request has an entity, the action would have already been processed in GetRequestStream. + if (request.Method == "GET") + { + this.action(request); + } + + return this.wrappedHandler.GetResponse(request); + } + + /// <summary> + /// Processes an <see cref="HttpWebRequest"/> and converts the + /// <see cref="HttpWebResponse"/> to a <see cref="IncomingWebResponse"/> instance. + /// </summary> + /// <param name="request">The <see cref="HttpWebRequest"/> to handle.</param> + /// <param name="options">The options to apply to this web request.</param> + /// <returns>An instance of <see cref="IncomingWebResponse"/> describing the response.</returns> + /// <exception cref="ProtocolException">Thrown for any network error.</exception> + /// <remarks> + /// <para>Implementations should catch <see cref="WebException"/> and wrap it in a + /// <see cref="ProtocolException"/> to abstract away the transport and provide + /// a single exception type for hosts to catch. The <see cref="WebException.Response"/> + /// value, if set, should be Closed before throwing.</para> + /// </remarks> + public IncomingWebResponse GetResponse(HttpWebRequest request, DirectWebRequestOptions options) + { + // If the request has an entity, the action would have already been processed in GetRequestStream. + if (request.Method == "GET") { + this.action(request); + } + + return this.wrappedHandler.GetResponse(request, options); + } + + #endregion + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs b/src/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs new file mode 100644 index 0000000..be0a650 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// <copyright file="WindowsLiveClient.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth2; + + public class WindowsLiveClient : WebServerClient { + private static readonly AuthorizationServerDescription WindowsLiveDescription = new AuthorizationServerDescription { + TokenEndpoint = new Uri("https://oauth.live.com/token"), + AuthorizationEndpoint = new Uri("https://oauth.live.com/authorize"), + }; + + /// <summary> + /// Initializes a new instance of the <see cref="WindowsLiveClient"/> class. + /// </summary> + public WindowsLiveClient() + : base(WindowsLiveDescription) { + this.AuthorizationTracker = new TokenManager(); + } + + /// <summary> + /// Well-known scopes defined by the Windows Live service. + /// </summary> + /// <remarks> + /// This sample includes just a few scopes. For a complete list of scopes please refer to: + /// http://msdn.microsoft.com/en-us/library/hh243646.aspx + /// </remarks> + public static class Scopes { + /// <summary> + /// The ability of an app to read and update a user's info at any time. Without this scope, an app can access the user's info only while the user is signed in to Live Connect and is using your app. + /// </summary> + public const string OfflineAccess = "wl.offline_access"; + + /// <summary> + /// Single sign-in behavior. With single sign-in, users who are already signed in to Live Connect are also signed in to your website. + /// </summary> + public const string SignIn = "wl.signin"; + + /// <summary> + /// Read access to a user's basic profile info. Also enables read access to a user's list of contacts. + /// </summary> + public const string Basic = "wl.basic"; + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs b/src/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs new file mode 100644 index 0000000..4801226 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="WindowsLiveGraph.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Runtime.Serialization; + using System.Runtime.Serialization.Json; + using System.Text; + + [DataContract] + public class WindowsLiveGraph { + private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(WindowsLiveGraph)); + + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "first_name")] + public string FirstName { get; set; } + + [DataMember(Name = "last_name")] + public string LastName { get; set; } + + [DataMember(Name = "link")] + public Uri Link { get; set; } + + [DataMember(Name = "gender")] + public string Gender { get; set; } + + [DataMember(Name = "updated_time")] + public string UpdatedTime { get; set; } + + [DataMember(Name = "locale")] + public string Locale { get; set; } + + public static WindowsLiveGraph Deserialize(string json) { + if (string.IsNullOrEmpty(json)) { + throw new ArgumentNullException("json"); + } + + return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json))); + } + + public static WindowsLiveGraph Deserialize(Stream jsonStream) { + if (jsonStream == null) { + throw new ArgumentNullException("jsonStream"); + } + + return (WindowsLiveGraph)jsonSerializer.ReadObject(jsonStream); + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/YammerConsumer.cs b/src/DotNetOpenAuth.ApplicationBlock/YammerConsumer.cs new file mode 100644 index 0000000..bbeb861 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/YammerConsumer.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// <copyright file="YammerConsumer.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + public static class YammerConsumer { + /// <summary> + /// The Consumer to use for accessing Google data APIs. + /// </summary> + public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription { + RequestTokenEndpoint = new MessageReceivingEndpoint("https://www.yammer.com/oauth/request_token", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.PostRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.yammer.com/oauth/authorize", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.GetRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("https://www.yammer.com/oauth/access_token", HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.PostRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new PlaintextSigningBindingElement() }, + ProtocolVersion = ProtocolVersion.V10, + }; + + public static DesktopConsumer CreateConsumer(IConsumerTokenManager tokenManager) { + return new DesktopConsumer(ServiceDescription, tokenManager); + } + + public static Uri PrepareRequestAuthorization(DesktopConsumer consumer, out string requestToken) { + if (consumer == null) { + throw new ArgumentNullException("consumer"); + } + + Uri authorizationUrl = consumer.RequestUserAuthorization(null, null, out requestToken); + return authorizationUrl; + } + + public static AuthorizedTokenResponse CompleteAuthorization(DesktopConsumer consumer, string requestToken, string userCode) { + // Because Yammer has a proprietary callback_token parameter, and it's passed + // with the message that specifically bans extra arguments being passed, we have + // to cheat by adding the data to the URL itself here. + var customServiceDescription = new ServiceProviderDescription { + RequestTokenEndpoint = ServiceDescription.RequestTokenEndpoint, + UserAuthorizationEndpoint = ServiceDescription.UserAuthorizationEndpoint, + AccessTokenEndpoint = new MessageReceivingEndpoint(ServiceDescription.AccessTokenEndpoint.Location.AbsoluteUri + "?oauth_verifier=" + Uri.EscapeDataString(userCode), HttpDeliveryMethods.AuthorizationHeaderRequest | HttpDeliveryMethods.PostRequest), + TamperProtectionElements = ServiceDescription.TamperProtectionElements, + ProtocolVersion = ProtocolVersion.V10, + }; + + // To use a custom service description we also must create a new WebConsumer. + var customConsumer = new DesktopConsumer(customServiceDescription, consumer.TokenManager); + var response = customConsumer.ProcessUserAuthorization(requestToken, userCode); + return response; + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs b/src/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs new file mode 100644 index 0000000..d361373 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------- +// <copyright file="YubikeyRelyingParty.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Net; + using System.Text; + using System.Text.RegularExpressions; + + /// <summary> + /// The set of possible results from verifying a Yubikey token. + /// </summary> + public enum YubikeyResult { + /// <summary> + /// The OTP is valid. + /// </summary> + Ok, + + /// <summary> + /// The OTP is invalid format. + /// </summary> + BadOtp, + + /// <summary> + /// The OTP has already been seen by the service. + /// </summary> + ReplayedOtp, + + /// <summary> + /// The HMAC signature verification failed. + /// </summary> + /// <remarks> + /// This indicates a bug in the relying party code. + /// </remarks> + BadSignature, + + /// <summary> + /// The request lacks a parameter. + /// </summary> + /// <remarks> + /// This indicates a bug in the relying party code. + /// </remarks> + MissingParameter, + + /// <summary> + /// The request id does not exist. + /// </summary> + NoSuchClient, + + /// <summary> + /// The request id is not allowed to verify OTPs. + /// </summary> + OperationNotAllowed, + + /// <summary> + /// Unexpected error in our server. Please contact Yubico if you see this error. + /// </summary> + BackendError, + } + + /// <summary> + /// Provides verification of a Yubikey one-time password (OTP) as a means of authenticating + /// a user at your web site or application. + /// </summary> + /// <remarks> + /// Please visit http://yubico.com/ for more information about this authentication method. + /// </remarks> + public class YubikeyRelyingParty { + /// <summary> + /// The default Yubico authorization server to use for validation and replay protection. + /// </summary> + private const string DefaultYubicoAuthorizationServer = "https://api.yubico.com/wsapi/verify"; + + /// <summary> + /// The format of the lines in the Yubico server response. + /// </summary> + private static readonly Regex ResultLineMatcher = new Regex(@"^(?<key>[^=]+)=(?<value>.*)$"); + + /// <summary> + /// The Yubico authorization server to use for validation and replay protection. + /// </summary> + private readonly string yubicoAuthorizationServer; + + /// <summary> + /// The authorization ID assigned to your individual site by Yubico. + /// </summary> + private readonly int yubicoAuthorizationId; + + /// <summary> + /// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class + /// that uses the default Yubico server for validation and replay protection. + /// </summary> + /// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico. + /// Get one from https://upgrade.yubico.com/getapikey/</param> + public YubikeyRelyingParty(int authorizationId) + : this(authorizationId, DefaultYubicoAuthorizationServer) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class. + /// </summary> + /// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico. + /// Contact tech@yubico.com if you haven't got an authId for your site.</param> + /// <param name="yubicoAuthorizationServer">The Yubico authorization server to use for validation and replay protection.</param> + public YubikeyRelyingParty(int authorizationId, string yubicoAuthorizationServer) { + if (authorizationId < 0) { + throw new ArgumentOutOfRangeException("authorizationId"); + } + + if (!Uri.IsWellFormedUriString(yubicoAuthorizationServer, UriKind.Absolute)) { + throw new ArgumentException("Invalid authorization server URI", "yubicoAuthorizationServer"); + } + + if (!yubicoAuthorizationServer.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("HTTPS is required for the Yubico server. HMAC response verification not supported.", "yubicoAuthorizationServer"); + } + + this.yubicoAuthorizationId = authorizationId; + this.yubicoAuthorizationServer = yubicoAuthorizationServer; + } + + /// <summary> + /// Extracts the username out of a Yubikey token. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + /// <returns>A 12 character string that is unique for this particular Yubikey device.</returns> + public static string ExtractUsername(string yubikeyToken) { + EnsureWellFormedToken(yubikeyToken); + return yubikeyToken.Substring(0, 12); + } + + /// <summary> + /// Determines whether the specified yubikey token is valid and has not yet been used. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + /// <returns> + /// <c>true</c> if the specified yubikey token is valid; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="WebException">Thrown when the validity of the token could not be confirmed due to network issues.</exception> + public YubikeyResult IsValid(string yubikeyToken) { + EnsureWellFormedToken(yubikeyToken); + + StringBuilder authorizationUri = new StringBuilder(this.yubicoAuthorizationServer); + authorizationUri.Append("?id="); + authorizationUri.Append(Uri.EscapeDataString(this.yubicoAuthorizationId.ToString(CultureInfo.InvariantCulture))); + authorizationUri.Append("&otp="); + authorizationUri.Append(Uri.EscapeDataString(yubikeyToken)); + + var request = WebRequest.Create(authorizationUri.ToString()); + using (var response = request.GetResponse()) { + using (var responseReader = new StreamReader(response.GetResponseStream())) { + string line; + var result = new NameValueCollection(); + while ((line = responseReader.ReadLine()) != null) { + Match m = ResultLineMatcher.Match(line); + if (m.Success) { + result[m.Groups["key"].Value] = m.Groups["value"].Value; + } + } + + return ParseResult(result["status"]); + } + } + } + + /// <summary> + /// Parses the Yubico server result. + /// </summary> + /// <param name="status">The status field from the response.</param> + /// <returns>The enum value representing the result.</returns> + private static YubikeyResult ParseResult(string status) { + switch (status) { + case "OK": return YubikeyResult.Ok; + case "BAD_OTP": return YubikeyResult.BadOtp; + case "REPLAYED_OTP": return YubikeyResult.ReplayedOtp; + case "BAD_SIGNATURE": return YubikeyResult.BadSignature; + case "MISSING_PARAMETER": return YubikeyResult.MissingParameter; + case "NO_SUCH_CLIENT": return YubikeyResult.NoSuchClient; + case "OPERATION_NOT_ALLOWED": return YubikeyResult.OperationNotAllowed; + case "BACKEND_ERROR": return YubikeyResult.BackendError; + default: throw new ArgumentOutOfRangeException("status", status, "Unexpected status value."); + } + } + + /// <summary> + /// Ensures the OTP is well formed. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + private static void EnsureWellFormedToken(string yubikeyToken) { + if (yubikeyToken == null) { + throw new ArgumentNullException("yubikeyToken"); + } + + yubikeyToken = yubikeyToken.Trim(); + + if (yubikeyToken.Length <= 12) { + throw new ArgumentException("Yubikey token has unexpected length."); + } + } + } +} diff --git a/src/DotNetOpenAuth.ApplicationBlock/packages.config b/src/DotNetOpenAuth.ApplicationBlock/packages.config new file mode 100644 index 0000000..4766339 --- /dev/null +++ b/src/DotNetOpenAuth.ApplicationBlock/packages.config @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="CodeContracts.Unofficial" version="1.0.0.2" />
+ <package id="DotNetOpenAuth.Core" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.InfoCard" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OAuth.Common" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OAuth.Consumer" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OAuth.Core" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OAuth.ServiceProvider" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OAuth2.Client" version="0.23.0-draft" />
+ <package id="DotNetOpenAuth.OAuth2.Client" version="0.23.0-draft2" />
+ <package id="DotNetOpenAuth.OAuth2.Client.UI" version="0.23.0-draft" />
+ <package id="DotNetOpenAuth.OAuth2.Client.UI" version="0.23.0-draft2" />
+ <package id="DotNetOpenAuth.OAuth2.Core" version="0.23.0-draft" />
+ <package id="DotNetOpenAuth.OAuth2.Core" version="0.23.0-draft2" />
+ <package id="DotNetOpenAuth.OpenId.Core" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OpenId.Provider" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OpenId.RelyingParty" version="4.0.0.12084" />
+ <package id="DotNetOpenAuth.OpenIdOAuth" version="4.0.0.12084" />
+</packages>
\ No newline at end of file |