diff options
Diffstat (limited to 'src')
46 files changed, 3920 insertions, 15 deletions
diff --git a/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs index 205d331..d37e92d 100644 --- a/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs +++ b/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs @@ -65,6 +65,7 @@ using System.Web.UI; [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.ResourceServer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.Client.UI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.Web.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] #else [assembly: InternalsVisibleTo("DotNetOpenAuth.Test")] diff --git a/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs index 49ef570..dffd281 100644 --- a/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs +++ b/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs @@ -52,6 +52,7 @@ using System.Web.UI; [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.Consumer, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.ServiceProvider, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.Web.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] #else [assembly: InternalsVisibleTo("DotNetOpenAuth.Test")] diff --git a/src/DotNetOpenAuth.Web.Test/DotNetOpenAuth.Web.Test.csproj b/src/DotNetOpenAuth.Web.Test/DotNetOpenAuth.Web.Test.csproj new file mode 100644 index 0000000..d02c530 --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/DotNetOpenAuth.Web.Test.csproj @@ -0,0 +1,87 @@ +<?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> + <StyleCopEnabled>False</StyleCopEnabled> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>8.0.30703</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth.Web.Test</RootNamespace> + <AssemblyName>DotNetOpenAuth.Web.Test</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </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> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <PropertyGroup> + <DelaySign>true</DelaySign> + </PropertyGroup> + <ItemGroup> + <Reference Include="Moq"> + <HintPath>..\..\lib\Moq.dll</HintPath> + </Reference> + <Reference Include="nunit.framework"> + <HintPath>..\..\lib\nunit.framework.dll</HintPath> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Web" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="OAuth2ClientTest.cs" /> + <Compile Include="OAuthAuthenticationTickerHelperTest.cs" /> + <Compile Include="OAuthClientTest.cs" /> + <Compile Include="OAuthWebSecurityTest.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="UriHelperTest.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\DotNetOpenAuth.Messaging\DotNetOpenAuth.Messaging.csproj"> + <Project>{60426312-6AE5-4835-8667-37EDEA670222}</Project> + <Name>DotNetOpenAuth.Messaging</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OAuth\DotNetOpenAuth.OAuth.csproj"> + <Project>{A288FCC8-6FCF-46DA-A45E-5F9281556361}</Project> + <Name>DotNetOpenAuth.OAuth</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.WebPages\DotNetOpenAuth.WebPages.csproj"> + <Project>{AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}</Project> + <Name>DotNetOpenAuth.WebPages</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.Web\DotNetOpenAuth.Web.csproj"> + <Project>{51835086-9611-4C53-819B-F2D5C9320873}</Project> + <Name>DotNetOpenAuth.Web</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <None Include="official-build-key.pfx" /> + </ItemGroup> + <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.Web.Test/OAuth2ClientTest.cs b/src/DotNetOpenAuth.Web.Test/OAuth2ClientTest.cs new file mode 100644 index 0000000..b090346 --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/OAuth2ClientTest.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using DotNetOpenAuth.Web.Clients; +using Moq; +using System.Web; +using System.Collections.Specialized; + +namespace DotNetOpenAuth.Web.Test +{ + [TestFixture] + public class OAuth2ClientTest + { + [TestCase] + public void TestProviderName() + { + // Arrange + var client = new MockOAuth2Client(); + + // Act + string providerName = client.ProviderName; + + // Assert + Assert.AreEqual("mockprovider", providerName); + } + + [TestCase] + public void RequestAuthenticationIssueCorrectRedirect() + { + // Arrange + var client = new MockOAuth2Client(); + var context = new Mock<HttpContextBase>(MockBehavior.Strict); + context.Setup(c => c.Response.Redirect("http://live.com/?q=http://return.to.me/", true)).Verifiable(); + + // Act + client.RequestAuthentication(context.Object, new Uri("http://return.to.me")); + + // Assert + context.Verify(); + } + + [TestCase] + public void VerifyAuthenticationThrowsIfContextIsNull() + { + // Arrange + var client = new MockOAuth2Client(); + + // Act && Assert + Assert.Throws<ArgumentNullException>(() => client.VerifyAuthentication(null)); + } + + [TestCase] + public void VerifyAuthenticationFailsIfCodeIsNotPresent() + { + // Arrange + var client = new MockOAuth2Client(); + var context = new Mock<HttpContextBase>(MockBehavior.Strict); + var queryStrings = new NameValueCollection(); + context.Setup(c => c.Request.QueryString).Returns(queryStrings); + + // Act + AuthenticationResult result = client.VerifyAuthentication(context.Object); + + // Assert + Assert.IsFalse(result.IsSuccessful); + } + + [TestCase] + public void VerifyAuthenticationFailsIfAccessTokenIsNull() + { + // Arrange + var client = new MockOAuth2Client(); + var context = new Mock<HttpContextBase>(MockBehavior.Strict); + var queryStrings = new NameValueCollection(); + queryStrings.Add("code", "random"); + context.Setup(c => c.Request.QueryString).Returns(queryStrings); + + // Act + AuthenticationResult result = client.VerifyAuthentication(context.Object); + + // Assert + Assert.IsFalse(result.IsSuccessful); + } + + [TestCase] + public void VerifyAuthenticationSucceeds() + { + // Arrange + var client = new MockOAuth2Client(); + var context = new Mock<HttpContextBase>(MockBehavior.Strict); + var queryStrings = new NameValueCollection(); + queryStrings.Add("code", "secret"); + context.Setup(c => c.Request.QueryString).Returns(queryStrings); + + // Act + AuthenticationResult result = client.VerifyAuthentication(context.Object); + + // Assert + Assert.True(result.IsSuccessful); + Assert.AreEqual("mockprovider", result.Provider); + Assert.AreEqual("12345", result.ProviderUserId); + Assert.AreEqual("John Doe", result.UserName); + Assert.NotNull(result.ExtraData); + Assert.AreEqual("abcde", result.ExtraData["token"]); + } + + private class MockOAuth2Client : OAuth2Client + { + public MockOAuth2Client() : base("mockprovider") + { + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + string url = "http://live.com/?q=" + returnUrl.ToString(); + return new Uri(url); + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + return (authorizationCode == "secret") ? "abcde" : null; + } + + protected override IDictionary<string, string> GetUserData(string accessToken) + { + if (accessToken == "abcde") + { + return new Dictionary<string, string> + { + {"id", "12345"}, + {"token", accessToken}, + {"name", "John Doe"} + }; + } + + return null; + } + } + } +} diff --git a/src/DotNetOpenAuth.Web.Test/OAuthAuthenticationTickerHelperTest.cs b/src/DotNetOpenAuth.Web.Test/OAuthAuthenticationTickerHelperTest.cs new file mode 100644 index 0000000..2545bb3 --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/OAuthAuthenticationTickerHelperTest.cs @@ -0,0 +1,152 @@ +using System; +using System.Web; +using System.Web.Security; +using DotNetOpenAuth.Web; +using Moq; +using NUnit.Framework; + +namespace DotNetOpenAuth.Test.Web +{ + [TestFixture] + public class OAuthAuthenticationTickerHelperTest + { + [TestCase] + public void SetAuthenticationTicketSetCookieOnHttpResponseWithPersistentSet() + { + SetAuthenticationTicketSetCookieOnHttpResponse(isPersistent: true); + } + + [TestCase] + public void SetAuthenticationTicketSetCookieOnHttpResponseWithPersistentNotSet() + { + SetAuthenticationTicketSetCookieOnHttpResponse(isPersistent: false); + } + + [TestCase] + public void IsOAuthAuthenticationTicketReturnsTrueIfCookieIsPresent() + { + // Arrange + var ticket = new FormsAuthenticationTicket( + 2, + "username", + DateTime.Now, + DateTime.Now.Add(FormsAuthentication.Timeout), + false, + "OAuth", + FormsAuthentication.FormsCookiePath); + + var cookie = new HttpCookie(name: FormsAuthentication.FormsCookieName, + value: FormsAuthentication.Encrypt(ticket)); + var cookies = new HttpCookieCollection {cookie}; + + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.Cookies).Returns(cookies); + + // Act + bool result = OAuthAuthenticationTicketHelper.IsOAuthAuthenticationTicket(context.Object); + + // Assert + Assert.IsTrue(result); + } + + [TestCase] + public void IsOAuthAuthenticationTicketReturnsFalseIfCookieIsNotPresent() + { + // Arrange + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.Cookies).Returns(new HttpCookieCollection()); + + // Act + bool result = OAuthAuthenticationTicketHelper.IsOAuthAuthenticationTicket(context.Object); + + // Assert + Assert.IsFalse(result); + } + + [TestCase] + public void IsOAuthAuthenticationTicketReturnsFalseIfCookieIsPresentButDoesNotHaveOAuthData() + { + // Arrange + var ticket = new FormsAuthenticationTicket( + 2, + "username", + DateTime.Now, + DateTime.Now.Add(FormsAuthentication.Timeout), + false, + null, + FormsAuthentication.FormsCookiePath); + + var cookie = new HttpCookie(name: FormsAuthentication.FormsCookieName, + value: FormsAuthentication.Encrypt(ticket)); + var cookies = new HttpCookieCollection { cookie }; + + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.Cookies).Returns(cookies); + + // Act + bool result = OAuthAuthenticationTicketHelper.IsOAuthAuthenticationTicket(context.Object); + + // Assert + Assert.IsFalse(result); + } + + [TestCase] + public void IsOAuthAuthenticationTicketReturnsFalseIfCookieIsPresentButDoesNotHaveCorrectName() + { + // Arrange + var response = new Mock<HttpResponseBase>(); + + var ticket = new FormsAuthenticationTicket( + 2, + "username", + DateTime.Now, + DateTime.Now.Add(FormsAuthentication.Timeout), + false, + "OAuth", + FormsAuthentication.FormsCookiePath); + + var cookie = new HttpCookie(name: "random cookie name", + value: FormsAuthentication.Encrypt(ticket)); + var cookies = new HttpCookieCollection { cookie }; + + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.Cookies).Returns(cookies); + + // Act + bool result = OAuthAuthenticationTicketHelper.IsOAuthAuthenticationTicket(context.Object); + + // Assert + Assert.IsFalse(result); + } + + private void SetAuthenticationTicketSetCookieOnHttpResponse(bool isPersistent) + { + // Arrange + var cookies = new HttpCookieCollection(); + + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.IsSecureConnection).Returns(true); + context.Setup(c => c.Response.Cookies).Returns(cookies); + + // Act + OAuthAuthenticationTicketHelper.SetAuthenticationTicket(context.Object, "user", isPersistent); + + // Assert + Assert.AreEqual(1, cookies.Count); + HttpCookie addedCookie = cookies[0]; + + Assert.AreEqual(FormsAuthentication.FormsCookieName, addedCookie.Name); + Assert.IsTrue(addedCookie.HttpOnly); + Assert.AreEqual("/", addedCookie.Path); + Assert.IsFalse(addedCookie.Secure); + Assert.IsNotNullOrEmpty(addedCookie.Value); + + FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(addedCookie.Value); + Assert.NotNull(ticket); + Assert.AreEqual(2, ticket.Version); + Assert.AreEqual("user", ticket.Name); + Assert.AreEqual("OAuth", ticket.UserData); + Assert.AreEqual(isPersistent, ticket.IsPersistent); + } + } +} diff --git a/src/DotNetOpenAuth.Web.Test/OAuthClientTest.cs b/src/DotNetOpenAuth.Web.Test/OAuthClientTest.cs new file mode 100644 index 0000000..8e8cf1e --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/OAuthClientTest.cs @@ -0,0 +1,142 @@ +using System; +using System.Web; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth.Messages; +using DotNetOpenAuth.Web.Clients; +using Moq; +using NUnit.Framework; + +namespace DotNetOpenAuth.Web.Test +{ + [TestFixture] + public class OAuthClientTest + { + [TestCase] + public void TestProviderNamePropertyIsCorrect() + { + // Arrange + var client = new MockOAuthClient(); + + // Act + var provider = client.ProviderName; + + // Assert + Assert.AreEqual("mockoauth", provider); + } + + [TestCase] + public void RequestAuthenticationInvokeMethodOnWebWorker() + { + // Arrange + var webWorker = new Mock<IOAuthWebWorker>(MockBehavior.Strict); + webWorker.Setup( + w => w.RequestAuthentication( + It.Is<Uri>(u => u.ToString().Equals("http://live.com/my/path.cshtml?q=one")))) + .Verifiable(); + + var client = new MockOAuthClient(webWorker.Object); + var returnUri = new Uri("http://live.com/my/path.cshtml?q=one"); + var context = new Mock<HttpContextBase>(); + + // Act + client.RequestAuthentication(context.Object, returnUri); + + // Assert + webWorker.Verify(); + } + + [TestCase] + public void VerifyAuthenticationFailsIfResponseTokenIsNull() + { + // Arrange + var webWorker = new Mock<IOAuthWebWorker>(MockBehavior.Strict); + webWorker.Setup(w => w.ProcessUserAuthorization()).Returns((AuthorizedTokenResponse)null); + + var client = new MockOAuthClient(webWorker.Object); + var context = new Mock<HttpContextBase>(); + + // Act + client.VerifyAuthentication(context.Object); + + // Assert + webWorker.Verify(); + } + + [TestCase] + public void VerifyAuthenticationFailsIfAccessTokenIsInvalid() + { + // Arrange + var endpoint = new MessageReceivingEndpoint("http://live.com/path/?a=b", HttpDeliveryMethods.GetRequest); + var request = new AuthorizedTokenRequest(endpoint, new Version("1.0")); + var response = new AuthorizedTokenResponse(request) + { + AccessToken = "invalid token" + }; + + var webWorker = new Mock<IOAuthWebWorker>(MockBehavior.Strict); + webWorker.Setup(w => w.ProcessUserAuthorization()).Returns(response).Verifiable(); + + var client = new MockOAuthClient(webWorker.Object); + var context = new Mock<HttpContextBase>(); + + // Act + AuthenticationResult result = client.VerifyAuthentication(context.Object); + + // Assert + webWorker.Verify(); + + Assert.False(result.IsSuccessful); + } + + [TestCase] + public void VerifyAuthenticationSucceeds() + { + // Arrange + var endpoint = new MessageReceivingEndpoint("http://live.com/path/?a=b", HttpDeliveryMethods.GetRequest); + var request = new AuthorizedTokenRequest(endpoint, new Version("1.0")); + var response = new AuthorizedTokenResponse(request) + { + AccessToken = "ok" + }; + + var webWorker = new Mock<IOAuthWebWorker>(MockBehavior.Strict); + webWorker.Setup(w => w.ProcessUserAuthorization()).Returns(response).Verifiable(); + + var client = new MockOAuthClient(webWorker.Object); + var context = new Mock<HttpContextBase>(); + + // Act + AuthenticationResult result = client.VerifyAuthentication(context.Object); + + // Assert + webWorker.Verify(); + + Assert.True(result.IsSuccessful); + Assert.AreEqual("mockoauth", result.Provider); + Assert.AreEqual("12345", result.ProviderUserId); + Assert.AreEqual("super", result.UserName); + } + + private class MockOAuthClient : OAuthClient + { + public MockOAuthClient() + : this(new Mock<IOAuthWebWorker>().Object) + { + } + + public MockOAuthClient(IOAuthWebWorker worker) : base("mockoauth", worker) + { + } + + protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response) + { + if (response.AccessToken == "ok") + { + return new AuthenticationResult(true, "mockoauth", "12345", "super", response.ExtraData); + } + + return AuthenticationResult.Failed; + } + } + } +} diff --git a/src/DotNetOpenAuth.Web.Test/OAuthWebSecurityTest.cs b/src/DotNetOpenAuth.Web.Test/OAuthWebSecurityTest.cs new file mode 100644 index 0000000..2465f0f --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/OAuthWebSecurityTest.cs @@ -0,0 +1,486 @@ +using System; +using System.Collections.Specialized; +using System.Web; +using System.Web.Security; +using DotNetOpenAuth.Web.Clients; +using Moq; +using NUnit.Framework; + +namespace DotNetOpenAuth.Web.Test +{ + [TestFixture] + public class OAuthWebSecurityTest + { + [TestCase] + public void RegisterDataProviderThrowsOnNullValue() + { + Assert.Throws(typeof(ArgumentNullException), () => OAuthWebSecurity.RegisterDataProvider(null)); + } + + [TestCase] + public void IsOAuthDataProviderIsFalseIfNotYetRegistered() + { + Assert.IsFalse(OAuthWebSecurity.IsOAuthDataProviderRegistered); + } + + [TestCase] + public void IsOAuthDataProviderIsTrueIfRegistered() + { + // Arrange + OAuthWebSecurity.RegisterDataProvider(new Mock<IOAuthDataProvider>().Object); + + // Act & Assert + Assert.IsTrue(OAuthWebSecurity.IsOAuthDataProviderRegistered); + } + + [TestCase] + public void RegisterDataProviderThrowsIfRegisterMoreThanOnce() + { + // Arrange + OAuthWebSecurity.RegisterDataProvider(new Mock<IOAuthDataProvider>().Object); + + // Act & Assert + Assert.Throws( + typeof(InvalidOperationException), + () => OAuthWebSecurity.RegisterDataProvider(new Mock<IOAuthDataProvider>().Object)); + } + + [TestCase] + public void RegisterClientThrowsOnNullValue() + { + Assert.Throws(typeof(ArgumentNullException), () => OAuthWebSecurity.RegisterClient(null)); + } + + [TestCase] + public void RegisterClientThrowsIfProviderNameIsEmpty() + { + // Arrange + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns((string)null); + + // Act & Assert + Assert.Throws(typeof(ArgumentException), () => OAuthWebSecurity.RegisterClient(client.Object), "Invalid provider name."); + + client.Setup(c => c.ProviderName).Returns(""); + + // Act & Assert + Assert.Throws(typeof(ArgumentException), () => OAuthWebSecurity.RegisterClient(client.Object), "Invalid provider name."); + } + + [TestCase] + public void RegisterClientThrowsRegisterMoreThanOneClientWithTheSameName() + { + // Arrange + var client1 = new Mock<IAuthenticationClient>(); + client1.Setup(c => c.ProviderName).Returns("provider"); + + var client2 = new Mock<IAuthenticationClient>(); + client2.Setup(c => c.ProviderName).Returns("provider"); + + OAuthWebSecurity.RegisterClient(client1.Object); + + // Act & Assert + Assert.Throws( + typeof(ArgumentException), + () => OAuthWebSecurity.RegisterClient(client2.Object), + "Another service provider with the same name has already been registered."); + } + + [TestCase] + public void RegisterOAuthClient() + { + // Arrange + var clients = new BuiltInOAuthClient[] + { + BuiltInOAuthClient.Facebook, + BuiltInOAuthClient.Twitter, + BuiltInOAuthClient.LinkedIn, + BuiltInOAuthClient.WindowsLive + }; + var clientNames = new string[] + { + "Facebook", + "Twitter", + "LinkedIn", + "WindowsLive" + }; + + for (int i = 0; i < clients.Length; i++) + { + // Act + OAuthWebSecurity.RegisterOAuthClient(clients[i], "key", "secret"); + + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns(clientNames[i]); + + // Assert + Assert.Throws( + typeof(ArgumentException), + () => OAuthWebSecurity.RegisterClient(client.Object), + "Another service provider with the same name has already been registered."); + } + } + + [TestCase] + public void RegisterOpenIDClient() + { + // Arrange + var clients = new BuiltInOpenIDClient[] + { + BuiltInOpenIDClient.Google, + BuiltInOpenIDClient.Yahoo + }; + var clientNames = new string[] + { + "Google", + "Yahoo" + }; + + for (int i = 0; i < clients.Length; i++) + { + // Act + OAuthWebSecurity.RegisterOpenIDClient(clients[i]); + + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns(clientNames[i]); + + // Assert + Assert.Throws( + typeof(ArgumentException), + () => OAuthWebSecurity.RegisterClient(client.Object), + "Another service provider with the same name has already been registered."); + } + } + + [TestCase] + public void RequestAuthenticationRedirectsToProviderWithNullReturnUrl() + { + // Arrange + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.ServerVariables).Returns( + new NameValueCollection()); + context.Setup(c => c.Request.Url).Returns(new Uri("http://live.com/login.aspx")); + context.Setup(c => c.Request.RawUrl).Returns("/login.aspx"); + + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns("windowslive"); + client.Setup(c => c.RequestAuthentication( + context.Object, + It.Is<Uri>(u => u.AbsoluteUri.Equals("http://live.com/login.aspx?__provider__=windowslive", StringComparison.OrdinalIgnoreCase)))) + .Verifiable(); + + OAuthWebSecurity.RegisterClient(client.Object); + + // Act + OAuthWebSecurity.RequestAuthenticationCore(context.Object, "windowslive", null); + + // Assert + client.Verify(); + } + + [TestCase] + public void RequestAuthenticationRedirectsToProviderWithReturnUrl() + { + // Arrange + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.ServerVariables).Returns( + new NameValueCollection()); + context.Setup(c => c.Request.Url).Returns(new Uri("http://live.com/login.aspx")); + context.Setup(c => c.Request.RawUrl).Returns("/login.aspx"); + + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns("yahoo"); + client.Setup(c => c.RequestAuthentication( + context.Object, + It.Is<Uri>(u => u.AbsoluteUri.Equals("http://yahoo.com/?__provider__=yahoo", StringComparison.OrdinalIgnoreCase)))) + .Verifiable(); + + OAuthWebSecurity.RegisterClient(client.Object); + + // Act + OAuthWebSecurity.RequestAuthenticationCore(context.Object, "yahoo", "http://yahoo.com"); + + // Assert + client.Verify(); + } + + [TestCase] + public void VerifyAuthenticationSucceed() + { + // Arrange + var queryStrings = new NameValueCollection(); + queryStrings.Add("__provider__", "facebook"); + + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.QueryString).Returns(queryStrings); + + var client = new Mock<IAuthenticationClient>(MockBehavior.Strict); + client.Setup(c => c.ProviderName).Returns("facebook"); + client.Setup(c => c.VerifyAuthentication(context.Object)).Returns(new AuthenticationResult(true, "facebook", "123", + "super", null)); + + var anotherClient = new Mock<IAuthenticationClient>(MockBehavior.Strict); + anotherClient.Setup(c => c.ProviderName).Returns("twitter"); + anotherClient.Setup(c => c.VerifyAuthentication(context.Object)).Returns(AuthenticationResult.Failed); + + OAuthWebSecurity.RegisterClient(client.Object); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act + AuthenticationResult result = OAuthWebSecurity.VerifyAuthenticationCore(context.Object); + + // Assert + Assert.True(result.IsSuccessful); + Assert.AreEqual("facebook", result.Provider); + Assert.AreEqual("123", result.ProviderUserId); + Assert.AreEqual("super", result.UserName); + Assert.IsNull(result.Error); + Assert.IsNull(result.ExtraData); + } + + [TestCase] + public void VerifyAuthenticationFail() + { + // Arrange + var queryStrings = new NameValueCollection(); + queryStrings.Add("__provider__", "twitter"); + + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.QueryString).Returns(queryStrings); + + var client = new Mock<IAuthenticationClient>(MockBehavior.Strict); + client.Setup(c => c.ProviderName).Returns("facebook"); + client.Setup(c => c.VerifyAuthentication(context.Object)).Returns(new AuthenticationResult(true, "facebook", "123", + "super", null)); + + var anotherClient = new Mock<IAuthenticationClient>(MockBehavior.Strict); + anotherClient.Setup(c => c.ProviderName).Returns("twitter"); + anotherClient.Setup(c => c.VerifyAuthentication(context.Object)).Returns(AuthenticationResult.Failed); + + OAuthWebSecurity.RegisterClient(client.Object); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act + AuthenticationResult result = OAuthWebSecurity.VerifyAuthenticationCore(context.Object); + + // Assert + Assert.False(result.IsSuccessful); + Assert.AreEqual("twitter", result.Provider); + } + + [TestCase] + public void VerifyAuthenticationFailIfNoProviderInQueryString() + { + // Arrange + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.QueryString).Returns(new NameValueCollection()); + + var client = new Mock<IAuthenticationClient>(MockBehavior.Strict); + client.Setup(c => c.ProviderName).Returns("facebook"); + + var anotherClient = new Mock<IAuthenticationClient>(MockBehavior.Strict); + anotherClient.Setup(c => c.ProviderName).Returns("twitter"); + + OAuthWebSecurity.RegisterClient(client.Object); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act + AuthenticationResult result = OAuthWebSecurity.VerifyAuthenticationCore(context.Object); + + // Assert + Assert.False(result.IsSuccessful); + Assert.IsNull(result.Provider); + } + + [TestCase] + public void LoginSetAuthenticationTicketIfSuccessful() + { + // Arrange + var cookies = new HttpCookieCollection(); + var context = new Mock<HttpContextBase>(); + context.Setup(c => c.Request.IsSecureConnection).Returns(true); + context.Setup(c => c.Response.Cookies).Returns(cookies); + + var dataProvider = new Mock<IOAuthDataProvider>(MockBehavior.Strict); + dataProvider.Setup(p => p.GetUserNameFromOAuth("twitter", "12345")).Returns("hola"); + OAuthWebSecurity.RegisterDataProvider(dataProvider.Object); + + // Act + bool successful = OAuthWebSecurity.LoginCore(context.Object, "twitter", "12345", createPersistentCookie: false); + + // Assert + Assert.IsTrue(successful); + + Assert.AreEqual(1, cookies.Count); + HttpCookie addedCookie = cookies[0]; + + Assert.AreEqual(FormsAuthentication.FormsCookieName, addedCookie.Name); + Assert.IsTrue(addedCookie.HttpOnly); + Assert.AreEqual("/", addedCookie.Path); + Assert.IsFalse(addedCookie.Secure); + Assert.IsNotNullOrEmpty(addedCookie.Value); + + FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(addedCookie.Value); + Assert.NotNull(ticket); + Assert.AreEqual(2, ticket.Version); + Assert.AreEqual("hola", ticket.Name); + Assert.AreEqual("OAuth", ticket.UserData); + Assert.IsFalse(ticket.IsPersistent); + } + + [TestCase] + public void LoginFailIfUserIsNotFound() + { + // Arrange + var context = new Mock<HttpContextBase>(); + + var dataProvider = new Mock<IOAuthDataProvider>(); + OAuthWebSecurity.RegisterDataProvider(dataProvider.Object); + + // Act + bool successful = OAuthWebSecurity.LoginCore(context.Object, "twitter", "12345", createPersistentCookie: false); + + // Assert + Assert.IsFalse(successful); + } + + [TestCase] + public void CreateOrUpdateAccountCallsOAuthDataProviderMethod() + { + // Arrange + var dataProvider = new Mock<IOAuthDataProvider>(MockBehavior.Strict); + OAuthWebSecurity.RegisterDataProvider(dataProvider.Object); + dataProvider.Setup(p => p.CreateOrUpdateOAuthAccount("twitter", "12345", "super")).Verifiable(); + + // Act + OAuthWebSecurity.CreateOrUpdateAccount("twitter", "12345", "super"); + + // Assert + dataProvider.Verify(); + } + + [TestCase] + public void GetAccountsFromUserNameCallsOAuthDataProviderMethod() + { + // Arrange + var accounts = new OAuthAccount[] + { + new OAuthAccount("twitter", "123"), + new OAuthAccount("facebook", "abc"), + new OAuthAccount("live", "xyz") + }; + + var dataProvider = new Mock<IOAuthDataProvider>(MockBehavior.Strict); + dataProvider.Setup(p => p.GetOAuthAccountsFromUserName("dotnetjunky")).Returns(accounts).Verifiable(); + + OAuthWebSecurity.RegisterDataProvider(dataProvider.Object); + + // Act + var retrievedAccounts = OAuthWebSecurity.GetAccountsFromUserName("dotnetjunky"); + + // Assert + CollectionAssert.AreEqual(retrievedAccounts, accounts); + } + + [TestCase] + public void DeleteAccountCallsOAuthDataProviderMethod() + { + // Arrange + var dataProvider = new Mock<IOAuthDataProvider>(MockBehavior.Strict); + OAuthWebSecurity.RegisterDataProvider(dataProvider.Object); + dataProvider.Setup(p => p.DeleteOAuthAccount("linkedin", "423432")).Returns(true).Verifiable(); + + // Act + OAuthWebSecurity.DeleteAccount("linkedin", "423432"); + + // Assert + dataProvider.Verify(); + } + + [TestCase] + public void GetOAuthClientReturnsTheCorrectClient() + { + // Arrange + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns("facebook"); + OAuthWebSecurity.RegisterClient(client.Object); + + var anotherClient = new Mock<IAuthenticationClient>(); + anotherClient.Setup(c => c.ProviderName).Returns("hulu"); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act + var expectedClient = OAuthWebSecurity.GetOAuthClient("facebook"); + + // Assert + Assert.AreSame(expectedClient, client.Object); + } + + [TestCase] + public void GetOAuthClientThrowsIfClientIsNotFound() + { + // Arrange + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns("facebook"); + OAuthWebSecurity.RegisterClient(client.Object); + + var anotherClient = new Mock<IAuthenticationClient>(); + anotherClient.Setup(c => c.ProviderName).Returns("hulu"); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act & Assert + Assert.Throws<ArgumentException>( + () => OAuthWebSecurity.GetOAuthClient("live"), + "A service provider could not be found by the specified name."); + } + + [TestCase] + public void TryGetOAuthClientSucceeds() + { + // Arrange + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns("facebook"); + OAuthWebSecurity.RegisterClient(client.Object); + + var anotherClient = new Mock<IAuthenticationClient>(); + anotherClient.Setup(c => c.ProviderName).Returns("hulu"); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act + IAuthenticationClient expectedClient; + bool result = OAuthWebSecurity.TryGetOAuthClient("facebook", out expectedClient); + + // Assert + Assert.AreSame(expectedClient, client.Object); + Assert.IsTrue(result); + } + + [TestCase] + public void TryGetOAuthClientFail() + { + // Arrange + var client = new Mock<IAuthenticationClient>(); + client.Setup(c => c.ProviderName).Returns("facebook"); + OAuthWebSecurity.RegisterClient(client.Object); + + var anotherClient = new Mock<IAuthenticationClient>(); + anotherClient.Setup(c => c.ProviderName).Returns("hulu"); + OAuthWebSecurity.RegisterClient(anotherClient.Object); + + // Act + IAuthenticationClient expectedClient; + bool result = OAuthWebSecurity.TryGetOAuthClient("live", out expectedClient); + + // Assert + Assert.IsNull(expectedClient); + Assert.IsFalse(result); + } + + [TearDown] + public void Cleanup() + { + OAuthWebSecurity.ClearDataProvider(); + OAuthWebSecurity.ClearProviders(); + } + } +} diff --git a/src/DotNetOpenAuth.Web.Test/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.Web.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2ea59e1 --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +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.Web.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft IT")] +[assembly: AssemblyProduct("DotNetOpenAuth.Web.Test")] +[assembly: AssemblyCopyright("Copyright © Microsoft IT 2011")] +[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("6c32d8f7-1394-40ef-9ec0-b8953adc0a4f")] + +// 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: AssemblyFileVersion("1.0.0.0")] diff --git a/src/DotNetOpenAuth.Web.Test/UriHelperTest.cs b/src/DotNetOpenAuth.Web.Test/UriHelperTest.cs new file mode 100644 index 0000000..557efae --- /dev/null +++ b/src/DotNetOpenAuth.Web.Test/UriHelperTest.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using DotNetOpenAuth.Web.Clients; + +namespace DotNetOpenAuth.Web.Test +{ + [TestFixture] + public class UriHelperTest + { + [TestCase] + public void TestAttachQueryStringParameterMethod() + { + // Arrange + string[] input = new string[] + { + "http://x.com", + "https://xxx.com/one?s=123", + "https://yyy.com/?s=6&u=a", + "https://zzz.com/default.aspx?name=sd" + }; + + string[] expectedOutput = new string[] + { + "http://x.com/?s=awesome", + "https://xxx.com/one?s=awesome", + "https://yyy.com/?s=awesome&u=a", + "https://zzz.com/default.aspx?name=sd&s=awesome" + }; + + for (int i = 0; i < input.Length; i++) + { + // Act + var inputUrl = new Uri(input[i]); + var outputUri = UriHelper.AttachQueryStringParameter(inputUrl, "s", "awesome"); + + // Assert + Assert.AreEqual(expectedOutput[i], outputUri.ToString()); + } + } + + [TestCase] + public void TestAppendQueryArguments() + { + // Arrange + var builder = new UriBuilder("http://www.microsoft.com"); + + // Act + builder.AppendQueryArguments( + new Dictionary<string, string> {{"one", "xxx"}, {"two", "yyy"}}); + + // Assert + if (builder.Port == 80) + { + // set port = -1 so that the display string doesn't contain port number + builder.Port = -1; + } + string s = builder.ToString(); + Assert.AreEqual("http://www.microsoft.com/?one=xxx&two=yyy", s); + } + } +} diff --git a/src/DotNetOpenAuth.Web/AuthenticationResult.cs b/src/DotNetOpenAuth.Web/AuthenticationResult.cs new file mode 100644 index 0000000..b32267d --- /dev/null +++ b/src/DotNetOpenAuth.Web/AuthenticationResult.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace DotNetOpenAuth.Web +{ + /// <summary> + /// Represents the result of OAuth & OpenId authentication + /// </summary> + public class AuthenticationResult + { + /// <summary> + /// Returns an instance which indicates failed authentication. + /// </summary> + [SuppressMessage( + "Microsoft.Security", + "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "This type is immutable.")] + public static readonly AuthenticationResult Failed = new AuthenticationResult(isSuccessful: false); + + /// <summary> + /// Gets a value indicating whether the authentication step is successful. + /// </summary> + /// <value> + /// <c>true</c> if authentication is successful; otherwise, <c>false</c>. + /// </value> + public bool IsSuccessful { get; private set; } + + /// <summary> + /// Gets the provider's name. + /// </summary> + public string Provider { get; private set; } + + /// <summary> + /// Gets the unique user id that is returned from the provider. + /// </summary> + public string ProviderUserId { get; private set; } + + /// <summary> + /// Gets the user name that is returned from the provider. + /// </summary> + public string UserName { get; private set; } + + /// <summary> + /// Gets the optional extra data that may be returned from the provider + /// </summary> + public IDictionary<string, string> ExtraData { get; private set; } + + /// <summary> + /// Gets the error that may have occured during the authentication process + /// </summary> + public Exception Error { get; private set; } + + public AuthenticationResult(bool isSuccessful) : + this(isSuccessful, + provider: null, + providerUserId: null, + userName: null, + extraData: null) + { + } + + public AuthenticationResult(Exception exception) : this(isSuccessful: false) + { + if (exception == null) + { + throw new ArgumentNullException("exception"); + } + + Error = exception; + } + + public AuthenticationResult( + bool isSuccessful, + string provider, + string providerUserId, + string userName, + IDictionary<string, string> extraData) + { + IsSuccessful = isSuccessful; + Provider = provider; + ProviderUserId = providerUserId; + UserName = userName; + ExtraData = extraData; + } + } +} diff --git a/src/DotNetOpenAuth.Web/BuiltInAuthenticationClient.cs b/src/DotNetOpenAuth.Web/BuiltInAuthenticationClient.cs new file mode 100644 index 0000000..29aea59 --- /dev/null +++ b/src/DotNetOpenAuth.Web/BuiltInAuthenticationClient.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; + +namespace DotNetOpenAuth.Web +{ + /// <summary> + /// Represents built in OAuth clients. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "OAuth")] + public enum BuiltInOAuthClient + { + Twitter, + Facebook, + LinkedIn, + WindowsLive + } +} diff --git a/src/DotNetOpenAuth.Web/BuiltInOpenIDClient.cs b/src/DotNetOpenAuth.Web/BuiltInOpenIDClient.cs new file mode 100644 index 0000000..65de6e3 --- /dev/null +++ b/src/DotNetOpenAuth.Web/BuiltInOpenIDClient.cs @@ -0,0 +1,11 @@ +namespace DotNetOpenAuth.Web +{ + /// <summary> + /// Represents built in OpenID clients. + /// </summary> + public enum BuiltInOpenIDClient + { + Google, + Yahoo + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/AuthenticationClientCollection.cs b/src/DotNetOpenAuth.Web/Clients/AuthenticationClientCollection.cs new file mode 100644 index 0000000..8f730e1 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/AuthenticationClientCollection.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.ObjectModel; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// A collection to store instances of IAuthenticationClient by keying off ProviderName. + /// </summary> + internal sealed class AuthenticationClientCollection : KeyedCollection<string, IAuthenticationClient> + { + public AuthenticationClientCollection() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + protected override string GetKeyForItem(IAuthenticationClient item) + { + return item.ProviderName; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/DictionaryExtensions.cs b/src/DotNetOpenAuth.Web/Clients/DictionaryExtensions.cs new file mode 100644 index 0000000..517fc47 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/DictionaryExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace DotNetOpenAuth.Web.Clients +{ + internal static class DictionaryExtensions + { + /// <summary> + /// Adds a key/value pair to the specified dictionary if the value is not null or empty. + /// </summary> + /// <param name="dictionary">The dictionary.</param> + /// <param name="key">The key.</param> + /// <param name="value">The value.</param> + public static void AddItemIfNotEmpty(this IDictionary<string, string> dictionary, string key, string value) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + if (!String.IsNullOrEmpty(value)) + { + dictionary[key] = value; + } + } + + /// <summary> + /// Adds the value from an XDocument with the specified element name if it's not empty. + /// </summary> + /// <param name="dictionary">The dictionary.</param> + /// <param name="document">The document.</param> + /// <param name="elementName">Name of the element.</param> + public static void AddDataIfNotEmpty( + this Dictionary<string, string> dictionary, + XDocument document, + string elementName) + { + var element = document.Root.Element(elementName); + if (element != null) + { + dictionary.AddItemIfNotEmpty(elementName, element.Value); + } + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/IAuthenticationClient.cs b/src/DotNetOpenAuth.Web/Clients/IAuthenticationClient.cs new file mode 100644 index 0000000..526d775 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/IAuthenticationClient.cs @@ -0,0 +1,33 @@ +using System; +using System.Web; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents a client which can authenticate users via an external website/provider. + /// </summary> + public interface IAuthenticationClient + { + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + string ProviderName { get; } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="context">The context of the current request.</param> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + void RequestAuthentication(HttpContextBase context, Uri returnUrl); + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="context">The context of the current request.</param> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + AuthenticationResult VerifyAuthentication(HttpContextBase context); + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/DotNetOpenAuthWebConsumer.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/DotNetOpenAuthWebConsumer.cs new file mode 100644 index 0000000..b66f0b1 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/DotNetOpenAuthWebConsumer.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Net; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + public class DotNetOpenAuthWebConsumer : IOAuthWebWorker + { + private readonly WebConsumer _webConsumer; + + public DotNetOpenAuthWebConsumer(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) + { + if (serviceDescription == null) + { + throw new ArgumentNullException("consumer"); + } + + if (tokenManager == null) + { + throw new ArgumentNullException("tokenManager"); + } + + _webConsumer = new WebConsumer(serviceDescription, tokenManager); + } + + public void RequestAuthentication(Uri callback) + { + var redirectParameters = new Dictionary<string, string>() { { "force_login", "false" } }; + UserAuthorizationRequest request = _webConsumer.PrepareRequestUserAuthorization(callback, null, redirectParameters); + _webConsumer.Channel.PrepareResponse(request).Send(); + } + + public AuthorizedTokenResponse ProcessUserAuthorization() + { + return _webConsumer.ProcessUserAuthorization(); + } + + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken) + { + return _webConsumer.PrepareAuthorizedRequest(profileEndpoint, accessToken); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/IOAuthWebWorker.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/IOAuthWebWorker.cs new file mode 100644 index 0000000..7ffc6b4 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/IOAuthWebWorker.cs @@ -0,0 +1,14 @@ +using System; +using System.Net; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + public interface IOAuthWebWorker + { + void RequestAuthentication(Uri callback); + AuthorizedTokenResponse ProcessUserAuthorization(); + HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/InMemoryOAuthTokenManager.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/InMemoryOAuthTokenManager.cs new file mode 100644 index 0000000..0cc1fba --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/InMemoryOAuthTokenManager.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// An implementation of IOAuthTokenManager which stores keys in memory. + /// </summary> + public sealed class InMemoryOAuthTokenManager : IConsumerTokenManager + { + private readonly Dictionary<string, string> _tokensAndSecrets = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="InMemoryOAuthTokenManager"/> class. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + public InMemoryOAuthTokenManager(string consumerKey, string consumerSecret) + { + if (consumerKey == null) + { + throw new ArgumentNullException("consumerKey"); + } + + if (consumerSecret == null) + { + throw new ArgumentNullException("consumerSecret"); + } + + ConsumerKey = consumerKey; + ConsumerSecret = consumerSecret; + } + + /// <summary> + /// Gets the consumer key. + /// </summary> + public string ConsumerKey + { + get; + private set; + } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + 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 _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) + { + _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) + { + _tokensAndSecrets.Remove(requestToken); + _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 + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/LinkedInClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/LinkedInClient.cs new file mode 100644 index 0000000..ddbc39f --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/LinkedInClient.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Xml.Linq; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents LinkedIn authentication client. + /// </summary> + internal sealed class LinkedInClient : OAuthClient + { + public static readonly ServiceProviderDescription LinkedInServiceDescription = new ServiceProviderDescription + { + RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> + /// Initializes a new instance of the <see cref="LinkedInClient"/> class. + /// </summary> + /// <param name="consumerKey">The LinkedIn app's consumer key.</param> + /// <param name="consumerSecret">The LinkedIn app's consumer secret.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "We can't dispose the object because we still need it through the app lifetime.")] + public LinkedInClient(string consumerKey, string consumerSecret) : + base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) + { + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="response">The response token returned from service provider</param> + /// <returns> + /// Authentication result. + /// </returns> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Design", + "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "We don't care if the request fails.")] + protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response) + { + // See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014 + const string profileRequestUrl = "http://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary)"; + + string accessToken = response.AccessToken; + + var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest); + HttpWebRequest request = WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken); + + try + { + using (WebResponse profileResponse = request.GetResponse()) + { + using (Stream responseStream = profileResponse.GetResponseStream()) + { + XDocument document = XDocument.Load(responseStream); + string userId = document.Root.Element("id").Value; + + string firstName = document.Root.Element("first-name").Value; + string lastName = document.Root.Element("last-name").Value; + string userName = firstName + " " + lastName; + + var extraData = new Dictionary<string, string>(); + extraData.Add("name", userName); + extraData.AddDataIfNotEmpty(document, "headline"); + extraData.AddDataIfNotEmpty(document, "summary"); + extraData.AddDataIfNotEmpty(document, "industry"); + + return new AuthenticationResult( + isSuccessful: true, + provider: ProviderName, + providerUserId: userId, + userName: userName, + extraData: extraData); + } + } + } + catch (Exception exception) + { + return new AuthenticationResult(exception); + } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/OAuthClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/OAuthClient.cs new file mode 100644 index 0000000..d0d7751 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/OAuthClient.cs @@ -0,0 +1,116 @@ +using System; +using System.Web; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents base class for OAuth 1.0 clients + /// </summary> + public abstract class OAuthClient : IAuthenticationClient + { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthClient"/> class. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="serviceDescription">The service description.</param> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + protected OAuthClient(string providerName, ServiceProviderDescription serviceDescription, string consumerKey, string consumerSecret) : + this(providerName, serviceDescription, new InMemoryOAuthTokenManager(consumerKey, consumerSecret)) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthClient"/> class. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + protected OAuthClient(string providerName, ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) : + this(providerName, new DotNetOpenAuthWebConsumer(serviceDescription, tokenManager)) + { + } + + protected OAuthClient(string providerName, IOAuthWebWorker webWorker) + { + if (providerName == null) + { + throw new ArgumentNullException("providerName"); + } + + if (webWorker == null) + { + throw new ArgumentNullException("webWorker"); + } + + ProviderName = providerName; + WebWorker = webWorker; + } + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName + { + get; + private set; + } + + /// <summary> + /// Gets the <see cref="OAuthWebConsumer"/> instance which handles constructing requests + /// to the OAuth providers. + /// </summary> + protected IOAuthWebWorker WebWorker + { + get; + private set; + } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + if (returnUrl == null) + { + throw new ArgumentNullException("returnUrl"); + } + + if (context == null) + { + throw new ArgumentNullException("context"); + } + + Uri callback = returnUrl.StripQueryArgumentsWithPrefix("oauth_"); + WebWorker.RequestAuthentication(callback); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + AuthorizedTokenResponse response = WebWorker.ProcessUserAuthorization(); + if (response == null) + { + return AuthenticationResult.Failed; + } + + return VerifyAuthenticationCore(response); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="response">The response token returned from service provider</param> + /// <returns>Authentication result</returns> + protected abstract AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/TwitterClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/TwitterClient.cs new file mode 100644 index 0000000..6d82607 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/TwitterClient.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Xml.Linq; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents a Twitter client + /// </summary> + internal class TwitterClient : OAuthClient + { + /// <summary> + /// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature. + /// </summary> + public static readonly ServiceProviderDescription TwitterServiceDescription = 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> + /// Initializes a new instance of the <see cref="TwitterClient"/> class with the specified consumer key and consumer secret. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "We can't dispose the object because we still need it through the app lifetime.")] + public TwitterClient(string consumerKey, string consumerSecret) : + base("twitter", TwitterServiceDescription, consumerKey, consumerSecret) + { + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="response">The response token returned from service provider</param> + /// <returns> + /// Authentication result + /// </returns> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Design", + "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "We don't care if the request for additional data fails.")] + protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response) + { + string accessToken = response.AccessToken; + string userId = response.ExtraData["user_id"]; + string userName = response.ExtraData["screen_name"]; + + string profileRequestUrl = "http://api.twitter.com/1/users/show.xml?user_id=" + Uri.EscapeDataString(userId); + var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest); + HttpWebRequest request = WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken); + + var extraData = new Dictionary<string, string>(); + try + { + using (WebResponse profileResponse = request.GetResponse()) + { + using (Stream responseStream = profileResponse.GetResponseStream()) + { + XDocument document = XDocument.Load(responseStream); + extraData.AddDataIfNotEmpty(document, "name"); + extraData.AddDataIfNotEmpty(document, "location"); + extraData.AddDataIfNotEmpty(document, "description"); + extraData.AddDataIfNotEmpty(document, "url"); + } + } + } + catch (Exception) + { + // At this point, the authentication is already successful. + // Here we are just trying to get additional data if we can. + // If it fails, no problem. + } + + return new AuthenticationResult( + isSuccessful: true, + provider: ProviderName, + providerUserId: userId, + userName: userName, + extraData: extraData); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookClient.cs new file mode 100644 index 0000000..e9e0b80 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookClient.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Web; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web.Clients +{ + internal sealed class FacebookClient : OAuth2Client + { + private const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth"; + private const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token"; + + private readonly string _appId; + private readonly string _appSecret; + + public FacebookClient(string appId, string appSecret) + : base("facebook") + { + if (String.IsNullOrEmpty(appId)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "appId"), + "appId"); + } + + if (String.IsNullOrEmpty("appSecret")) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "appSecret"), + "appSecret"); + } + + _appId = appId; + _appSecret = appSecret; + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + // Note: Facebook doesn't like us to url-encode the redirect_uri value + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArguments(new Dictionary<string, string> + { + { "client_id", _appId }, + { "redirect_uri", returnUrl.ToString() } + }); + return builder.Uri; + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + // Note: Facebook doesn't like us to url-encode the redirect_uri value + var builder = new UriBuilder(TokenEndpoint); + builder.AppendQueryArguments(new Dictionary<string, string> + { + { "client_id", _appId }, + { "redirect_uri", returnUrl.ToString() }, + { "client_secret", _appSecret }, + { "code", authorizationCode } + }); + + using (WebClient client = new WebClient()) + { + string data = client.DownloadString(builder.Uri); + if (String.IsNullOrEmpty(data)) + { + return null; + } + + var parsedQueryString = HttpUtility.ParseQueryString(data); + if (parsedQueryString != null) + { + return parsedQueryString["access_token"]; + } + } + return null; + } + + protected override IDictionary<string, string> GetUserData(string accessToken) + { + FacebookGraph graph; + var request = WebRequest.Create("https://graph.facebook.com/me?access_token=" + Uri.EscapeDataString(accessToken)); + using (var response = request.GetResponse()) + { + using (var responseStream = response.GetResponseStream()) + { + graph = JsonHelper.Deserialize<FacebookGraph>(responseStream); + } + } + + // this dictionary must contains + var userData = new Dictionary<string, string>(); + userData.AddItemIfNotEmpty("id", graph.Id); + userData.AddItemIfNotEmpty("username", graph.Email); + userData.AddItemIfNotEmpty("name", graph.Name); + userData.AddItemIfNotEmpty("link", graph.Link == null ? null : graph.Link.ToString()); + userData.AddItemIfNotEmpty("gender", graph.Gender); + userData.AddItemIfNotEmpty("birthday", graph.Birthday); + return userData; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookGraph.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookGraph.cs new file mode 100644 index 0000000..43f31eb --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookGraph.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Contains data of a Facebook user. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable + /// in medium trust, it has to be public. + /// </remarks> + [DataContract] + public class FacebookGraph + { + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "email")] + public string Email { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "link")] + public Uri Link { get; set; } + + [DataMember(Name = "gender")] + public string Gender { get; set; } + + [DataMember(Name = "birthday")] + public string Birthday { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/JsonHelper.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/JsonHelper.cs new file mode 100644 index 0000000..26cc61a --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/JsonHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; +using System.Runtime.Serialization.Json; + +namespace DotNetOpenAuth.Web.Clients +{ + internal static class JsonHelper + { + public static T Deserialize<T>(Stream stream) where T : class + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + var serializer = new DataContractJsonSerializer(typeof(T)); + return (T)serializer.ReadObject(stream); + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2AccessTokenData.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2AccessTokenData.cs new file mode 100644 index 0000000..9649ad5 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2AccessTokenData.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace DotNetOpenAuth.Web.Clients +{ + [DataContract] + public class OAuth2AccessTokenData + { + [DataMember(Name = "access_token")] + public string AccessToken { get; set; } + + [DataMember(Name = "refresh_token")] + public string RefreshToken { get; set; } + + [DataMember(Name = "scope")] + public string Scope { get; set; } + + [DataMember(Name = "token_type")] + public string TokenType { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2Client.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2Client.cs new file mode 100644 index 0000000..9920065 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2Client.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Web; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents the base class for OAuth 2.0 clients + /// </summary> + public abstract class OAuth2Client : IAuthenticationClient + { + private readonly string _providerName; + private Uri _returnUrl; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2Client"/> class with the specified provider name. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + protected OAuth2Client(string providerName) + { + if (providerName == null) + { + throw new ArgumentNullException("providerName"); + } + + _providerName = providerName; + } + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName + { + get { return _providerName; } + } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (returnUrl == null) + { + throw new ArgumentNullException("returnUrl"); + } + + _returnUrl = returnUrl; + + string redirectUrl = GetServiceLoginUrl(returnUrl).ToString(); + context.Response.Redirect(redirectUrl, endResponse: true); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + string code = context.Request.QueryString["code"]; + if (String.IsNullOrEmpty(code)) + { + return AuthenticationResult.Failed; + } + + string accessToken = QueryAccessToken(_returnUrl, code); + if (accessToken == null) + { + return AuthenticationResult.Failed; + } + + IDictionary<string, string> userData = GetUserData(accessToken); + if (userData == null) + { + return AuthenticationResult.Failed; + } + string id = userData["id"]; + string name; + // Some oAuth providers do not return value for the 'username' attribute. + // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' + if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) + { + name = id; + } + + return new AuthenticationResult( + isSuccessful: true, + provider: ProviderName, + providerUserId: id, + userName: name, + extraData: userData); + } + + /// <summary> + /// Gets the full url pointing to the login page for this client. The url should include the + /// specified return url so that when the login completes, user is redirected back to that url. + /// </summary> + /// <param name="returnUrl">The return URL.</param> + /// <returns></returns> + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "Login is used more consistently in ASP.Net")] + protected abstract Uri GetServiceLoginUrl(Uri returnUrl); + + /// <summary> + /// Queries the access token from the specified authorization code. + /// </summary> + /// <param name="returnUrl">The return URL.</param> + /// <param name="authorizationCode">The authorization code.</param> + /// <returns></returns> + protected abstract string QueryAccessToken(Uri returnUrl, string authorizationCode); + + /// <summary> + /// Given the access token, gets the logged-in user's data. The returned dictionary must include + /// two keys 'id', and 'username'. + /// </summary> + /// <param name="accessToken">The access token of the current user.</param> + /// <returns>A dictionary contains key-value pairs of user data</returns> + protected abstract IDictionary<string, string> GetUserData(string accessToken); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveClient.cs new file mode 100644 index 0000000..9184be6 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveClient.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +namespace DotNetOpenAuth.Web.Clients +{ + internal sealed class WindowsLiveClient : OAuth2Client + { + private const string TokenEndpoint = "https://oauth.live.com/token"; + private const string AuthorizationEndpoint = "https://oauth.live.com/authorize"; + private readonly string _appId; + private readonly string _appSecret; + + public WindowsLiveClient(string appId, string appSecret) + : base("windowslive") + { + if (String.IsNullOrEmpty(appId)) + { + throw new ArgumentNullException("appId"); + } + + if (String.IsNullOrEmpty("appSecret")) + { + throw new ArgumentNullException("appSecret"); + } + + _appId = appId; + _appSecret = appSecret; + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArguments(new Dictionary<string, string> + { + { "client_id", _appId }, + { "scope", "wl.basic" }, + { "response_type", "code" }, + { "redirect_uri", returnUrl.ToString() } + }); + + return builder.Uri; + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + var builder = new StringBuilder(); + builder.AppendFormat("client_id={0}", _appId); + builder.AppendFormat("&redirect_uri={0}", Uri.EscapeDataString(returnUrl.ToString())); + builder.AppendFormat("&client_secret={0}", _appSecret); + builder.AppendFormat("&code={0}", authorizationCode); + builder.Append("&grant_type=authorization_code"); + + WebRequest tokenRequest = WebRequest.Create(TokenEndpoint); + tokenRequest.ContentType = "application/x-www-form-urlencoded"; + tokenRequest.ContentLength = builder.Length; + tokenRequest.Method = "POST"; + + using (Stream requestStream = tokenRequest.GetRequestStream()) + { + var writer = new StreamWriter(requestStream); + writer.Write(builder.ToString()); + writer.Flush(); + } + + HttpWebResponse tokenResponse = (HttpWebResponse)tokenRequest.GetResponse(); + if (tokenResponse.StatusCode == HttpStatusCode.OK) + { + using (Stream responseStream = tokenResponse.GetResponseStream()) + { + var tokenData = JsonHelper.Deserialize<OAuth2AccessTokenData>(responseStream); + if (tokenData != null) + { + return tokenData.AccessToken; + } + } + } + + return null; + } + + protected override IDictionary<string, string> GetUserData(string accessToken) + { + WindowsLiveUserData graph; + var request = WebRequest.Create("https://apis.live.net/v5.0/me?access_token=" + Uri.EscapeDataString(accessToken)); + using (var response = request.GetResponse()) + { + using (var responseStream = response.GetResponseStream()) + { + graph = JsonHelper.Deserialize<WindowsLiveUserData>(responseStream); + } + } + + var userData = new Dictionary<string, string>(); + userData.AddItemIfNotEmpty("id", graph.Id); + userData.AddItemIfNotEmpty("username", graph.Name); + userData.AddItemIfNotEmpty("name", graph.Name); + userData.AddItemIfNotEmpty("link", graph.Link == null ? null : graph.Link.ToString()); + userData.AddItemIfNotEmpty("gender", graph.Gender); + userData.AddItemIfNotEmpty("firstname", graph.FirstName); + userData.AddItemIfNotEmpty("lastname", graph.LastName); + return userData; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveUserData.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveUserData.cs new file mode 100644 index 0000000..cc6fa27 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveUserData.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Contains data of a Windows Live user. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable + /// in medium trust, it has to be public. + /// </remarks> + [DataContract] + public class WindowsLiveUserData + { + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "link")] + public Uri Link { get; set; } + + [DataMember(Name = "gender")] + public string Gender { get; set; } + + [DataMember(Name = "first_name")] + public string FirstName { get; set; } + + [DataMember(Name = "last_name")] + public string LastName { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/AxKnownAttributes.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/AxKnownAttributes.cs new file mode 100644 index 0000000..1afcc65 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/AxKnownAttributes.cs @@ -0,0 +1,12 @@ +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Contains namespace values of common attributes used for Attribute Exchange extensions + /// </summary> + internal static class AxKnownAttributes + { + public const string FirstName = "http://axschema.org/namePerson/first"; + public const string LastName = "http://axschema.org/namePerson/last"; + public const string FullName = "http://axschema.org/namePerson"; + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/GoogleOpenIdClient.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/GoogleOpenIdClient.cs new file mode 100644 index 0000000..61b88ee --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/GoogleOpenIdClient.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; +using DotNetOpenAuth.OpenId.RelyingParty; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents Google OpenID client. + /// </summary> + internal sealed class GoogleOpenIdClient : OpenIDClient + { + public GoogleOpenIdClient() : + base("google", "https://www.google.com/accounts/o8/id") + { + } + + /// <summary> + /// Called just before the authentication request is sent to service provider. + /// </summary> + /// <param name="request">The request.</param> + protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request) + { + // Attribute Exchange extensions + var fetchRequest = new FetchRequest(); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email, isRequired: true)); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.HomeAddress.Country, isRequired: false)); + fetchRequest.Attributes.Add(new AttributeRequest(AxKnownAttributes.FirstName, isRequired: false)); + fetchRequest.Attributes.Add(new AttributeRequest(AxKnownAttributes.LastName, isRequired: false)); + + request.AddExtension(fetchRequest); + } + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns></returns> + protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response) + { + FetchResponse fetchResponse = response.GetExtension<FetchResponse>(); + if (fetchResponse != null) + { + var extraData = new Dictionary<string, string>(); + extraData.AddItemIfNotEmpty("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email)); + extraData.AddItemIfNotEmpty("country", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country)); + extraData.AddItemIfNotEmpty("firstName", fetchResponse.GetAttributeValue(AxKnownAttributes.FirstName)); + extraData.AddItemIfNotEmpty("lastName", fetchResponse.GetAttributeValue(AxKnownAttributes.LastName)); + + return extraData; + } + + return null; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/OpenIDClient.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/OpenIDClient.cs new file mode 100644 index 0000000..f0f938e --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/OpenIDClient.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Web; +using DotNetOpenAuth.OpenId; +using DotNetOpenAuth.OpenId.RelyingParty; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Base classes for OpenID clients. + /// </summary> + internal class OpenIDClient : IAuthenticationClient + { + private readonly Identifier _providerIdentifier; + private readonly string _providerName; + + private static OpenIdRelyingParty _openidRelayingParty = + new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore()); + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIDClient"/> class. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="providerIdentifier">The provider identifier, which is the usually the login url of the specified provider.</param> + public OpenIDClient(string providerName, string providerIdentifier) + { + if (String.IsNullOrEmpty(providerIdentifier)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "providerIdentifier"), + "providerIdentifier"); + } + + if (String.IsNullOrEmpty(providerName)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "providerName"), + "providerName"); + } + + _providerName = providerName; + if (!Identifier.TryParse(providerIdentifier, out _providerIdentifier) || _providerIdentifier == null) + { + throw new ArgumentException(WebResources.OpenIDInvalidIdentifier, "providerIdentifier"); + } + } + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName + { + get + { + return _providerName; + } + } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Usage", + "CA2234:PassSystemUriObjectsInsteadOfStrings", + Justification = "We don't have a Uri object handy.")] + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + if (returnUrl == null) + { + throw new ArgumentNullException("returnUrl"); + } + + var realm = new Realm(returnUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)); + IAuthenticationRequest request = _openidRelayingParty.CreateRequest(_providerIdentifier, realm, returnUrl); + + // give subclasses a chance to modify request message, e.g. add extension attributes, etc. + OnBeforeSendingAuthenticationRequest(request); + + request.RedirectToProvider(); + } + + /// <summary> + /// Called just before the authentication request is sent to service provider. + /// </summary> + /// <param name="request">The request.</param> + protected virtual void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request) + { + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + IAuthenticationResponse response = _openidRelayingParty.GetResponse(); + if (response == null) + { + throw new InvalidOperationException(WebResources.OpenIDFailedToGetResponse); + } + + if (response.Status == AuthenticationStatus.Authenticated) + { + string id = response.ClaimedIdentifier; + string username; + + Dictionary<string, string> extraData = GetExtraData(response) ?? new Dictionary<string, string>(); + // try to look up username from the 'username' or 'email' property. If not found, fall back to 'friendly id' + if (!extraData.TryGetValue("username", out username) && !extraData.TryGetValue("email", out username)) + { + username = response.FriendlyIdentifierForDisplay; + } + + return new AuthenticationResult( + true, + ProviderName, + id, + username, + extraData); + } + + return AuthenticationResult.Failed; + } + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns></returns> + protected virtual Dictionary<string, string> GetExtraData(IAuthenticationResponse response) + { + return null; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/YahooOpenIdClient.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/YahooOpenIdClient.cs new file mode 100644 index 0000000..2235a2b --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/YahooOpenIdClient.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; +using DotNetOpenAuth.OpenId.RelyingParty; + +namespace DotNetOpenAuth.Web.Clients +{ + internal sealed class YahooOpenIdClient : OpenIDClient + { + public YahooOpenIdClient() : + base("yahoo", "http://me.yahoo.com") + { + } + + /// <summary> + /// Called just before the authentication request is sent to service provider. + /// </summary> + /// <param name="request">The request.</param> + protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request) + { + // Attribute Exchange extensions + var fetchRequest = new FetchRequest(); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email, isRequired: true)); + fetchRequest.Attributes.Add(new AttributeRequest(AxKnownAttributes.FullName, isRequired: false)); + + request.AddExtension(fetchRequest); + } + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns></returns> + protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response) + { + FetchResponse fetchResponse = response.GetExtension<FetchResponse>(); + if (fetchResponse != null) + { + var extraData = new Dictionary<string, string>(); + extraData.AddItemIfNotEmpty("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email)); + extraData.AddItemIfNotEmpty("fullName", fetchResponse.GetAttributeValue(AxKnownAttributes.FullName)); + + return extraData; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/UriHelper.cs b/src/DotNetOpenAuth.Web/Clients/UriHelper.cs new file mode 100644 index 0000000..f8f9723 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/UriHelper.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; +using DotNetOpenAuth.Messaging; + +namespace DotNetOpenAuth.Web.Clients +{ + internal static class UriHelper + { + /// <summary> + /// Attaches the query string '__provider' to an existing url. If the url already + /// contains the __provider query string, it overrides it with the specified provider name. + /// </summary> + /// <param name="url">The original url.</param> + /// <param name="providerName">Name of the provider.</param> + /// <returns>The new url with the provider name query string attached</returns> + /// <example> + /// If the url is: http://contoso.com, and providerName='facebook', the returned value is: http://contoso.com?__provider=facebook + /// If the url is: http://contoso.com?a=1, and providerName='twitter', the returned value is: http://contoso.com?a=1&__provider=twitter + /// If the url is: http://contoso.com?a=1&__provider=twitter, and providerName='linkedin', the returned value is: http://contoso.com?a=1&__provider=linkedin + /// </example> + /// <remarks> + /// The reason we have to do this is so that when the external service provider forwards user + /// back to our site, we know which provider it comes back from. + /// </remarks> + public static Uri AttachQueryStringParameter(this Uri url, string parameterName, string parameterValue) + { + UriBuilder builder = new UriBuilder(url); + string query = builder.Query; + if (query.Length > 1) + { + // remove the '?' character in front of the query string + query = query.Substring(1); + } + + string parameterPrefix = parameterName + "="; + + string encodedParameterValue = Uri.EscapeDataString(parameterValue); + + string newQuery = Regex.Replace(query, parameterPrefix + "[^\\&]*", parameterPrefix + encodedParameterValue); + if (newQuery == query) + { + if (newQuery.Length > 0) + { + newQuery += "&"; + } + newQuery = newQuery + parameterPrefix + encodedParameterValue; + } + builder.Query = newQuery; + + return builder.Uri; + } + + /// <summary> + /// Appends the specified key/value pairs as query string parameters to the builder. + /// </summary> + /// <param name="builder">The builder.</param> + /// <param name="pairs">The pairs.</param> + /// <returns></returns> + public static void AppendQueryArguments(this UriBuilder builder, IDictionary<string, string> pairs) + { + if (pairs == null) + { + throw new ArgumentNullException("pairs"); + } + + if (!pairs.Any()) + { + return; + } + + string query = builder.Query; + if (query.Length > 1) + { + // remove the '?' character in front of the query string and append the '&' + query = query.Substring(1); + } + + var sb = new StringBuilder(query); + foreach (KeyValuePair<string, string> pair in pairs) + { + if (sb.Length > 0) + { + sb.Append('&'); + } + sb.AppendFormat("{0}={1}", pair.Key, pair.Value); + } + + builder.Query = sb.ToString(); + } + + /// <summary> + /// Converts an app-relative url, e.g. ~/Content/Return.cshtml, to a full-blown url, e.g. http://mysite.com/Content/Return.cshtml + /// </summary> + /// <param name="returnUrl">The return URL.</param> + /// <returns></returns> + public static Uri ConvertToAbsoluteUri(string returnUrl) + { + if (Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute)) + { + return new Uri(returnUrl, UriKind.Absolute); + } + + if (HttpContext.Current == null) + { + return null; + } + + if (!VirtualPathUtility.IsAbsolute(returnUrl)) + { + returnUrl = VirtualPathUtility.ToAbsolute(returnUrl); + } + + return new Uri(GetPublicFacingUrl(new HttpRequestWrapper(HttpContext.Current.Request)), returnUrl); + } + + /// <summary> + /// Gets the public facing URL of this request as what clients see it. + /// </summary> + /// <param name="request">The request.</param> + public static Uri GetPublicFacingUrl(HttpRequestBase request) + { + NameValueCollection serverVariables = request.ServerVariables; + if (serverVariables["HTTP_HOST"] != null) + { + string forwardProto = serverVariables["HTTP_X_FORWARDED_PROTO"]; + if (forwardProto == null) + { + string scheme = request.Url.Scheme; + var hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); + var publicRequestUri = new UriBuilder(request.Url) + { + Scheme = scheme, + Host = hostAndPort.Host, + Port = hostAndPort.Port + }; + + return publicRequestUri.Uri; + } + } + return new Uri(request.Url, request.RawUrl); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/DotNetOpenAuth.Web.csproj b/src/DotNetOpenAuth.Web/DotNetOpenAuth.Web.csproj new file mode 100644 index 0000000..5f5aef8 --- /dev/null +++ b/src/DotNetOpenAuth.Web/DotNetOpenAuth.Web.csproj @@ -0,0 +1,116 @@ +<?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> + <StyleCopEnabled>False</StyleCopEnabled> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>8.0.30703</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{51835086-9611-4C53-819B-F2D5C9320873}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth.Web</RootNamespace> + <AssemblyName>DotNetOpenAuth.Web</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>..\..\bin\v4.0\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>ExtendedDesignGuidelineRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>..\..\bin\v4.0\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.Product.props" /> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Runtime.Serialization" /> + <Reference Include="System.Web" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="AuthenticationResult.cs" /> + <Compile Include="BuiltInAuthenticationClient.cs" /> + <Compile Include="BuiltInOpenIDClient.cs" /> + <Compile Include="Clients\AuthenticationClientCollection.cs" /> + <Compile Include="Clients\DictionaryExtensions.cs" /> + <Compile Include="Clients\IAuthenticationClient.cs" /> + <Compile Include="Clients\OAuth2\FacebookClient.cs" /> + <Compile Include="Clients\OAuth2\FacebookGraph.cs" /> + <Compile Include="Clients\OAuth2\JsonHelper.cs" /> + <Compile Include="Clients\OAuth2\OAuth2AccessTokenData.cs" /> + <Compile Include="Clients\OAuth2\OAuth2Client.cs" /> + <Compile Include="Clients\OAuth2\WindowsLiveClient.cs" /> + <Compile Include="Clients\OAuth2\WindowsLiveUserData.cs" /> + <Compile Include="Clients\OAuth\DotNetOpenAuthWebConsumer.cs" /> + <Compile Include="Clients\OAuth\InMemoryOAuthTokenManager.cs" /> + <Compile Include="Clients\OAuth\IOAuthWebWorker.cs" /> + <Compile Include="Clients\OAuth\LinkedInClient.cs" /> + <Compile Include="Clients\OAuth\OAuthClient.cs" /> + <Compile Include="Clients\OAuth\TwitterClient.cs" /> + <Compile Include="Clients\OpenID\AxKnownAttributes.cs" /> + <Compile Include="Clients\OpenID\GoogleOpenIdClient.cs" /> + <Compile Include="Clients\OpenID\OpenIDClient.cs" /> + <Compile Include="Clients\OpenID\YahooOpenIdClient.cs" /> + <Compile Include="Clients\UriHelper.cs" /> + <Compile Include="IOAuthDataProvider.cs" /> + <Compile Include="OAuthAccount.cs" /> + <Compile Include="OAuthAuthenticationTicketHelper.cs" /> + <Compile Include="OAuthWebSecurity.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Resources\WebResources.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>WebResources.resx</DependentUpon> + </Compile> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Resources\WebResources.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>WebResources.Designer.cs</LastGenOutput> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\DotNetOpenAuth.Messaging\DotNetOpenAuth.Messaging.csproj"> + <Project>{60426312-6AE5-4835-8667-37EDEA670222}</Project> + <Name>DotNetOpenAuth.Messaging</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OAuth.Consumer\DotNetOpenAuth.OAuth.Consumer.csproj"> + <Project>{B202E40D-4663-4A2B-ACDA-865F88FF7CAA}</Project> + <Name>DotNetOpenAuth.OAuth.Consumer</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OAuth\DotNetOpenAuth.OAuth.csproj"> + <Project>{A288FCC8-6FCF-46DA-A45E-5F9281556361}</Project> + <Name>DotNetOpenAuth.OAuth</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OpenId.RelyingParty\DotNetOpenAuth.OpenId.RelyingParty.csproj"> + <Project>{F458AB60-BA1C-43D9-8CEF-EC01B50BE87B}</Project> + <Name>DotNetOpenAuth.OpenId.RelyingParty</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OpenId\DotNetOpenAuth.OpenId.csproj"> + <Project>{3896A32A-E876-4C23-B9B8-78E17D134CD3}</Project> + <Name>DotNetOpenAuth.OpenId</Name> + </ProjectReference> + </ItemGroup> + <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.Web/IOAuthDataProvider.cs b/src/DotNetOpenAuth.Web/IOAuthDataProvider.cs new file mode 100644 index 0000000..cf2d576 --- /dev/null +++ b/src/DotNetOpenAuth.Web/IOAuthDataProvider.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace DotNetOpenAuth.Web +{ + public interface IOAuthDataProvider + { + string GetUserNameFromOAuth(string oAuthProvider, string oAuthId); + + void CreateOrUpdateOAuthAccount(string oAuthProvider, string oAuthId, string userName); + + bool DeleteOAuthAccount(string oAuthProvider, string oAuthId); + + ICollection<OAuthAccount> GetOAuthAccountsFromUserName(string userName); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/OAuthAccount.cs b/src/DotNetOpenAuth.Web/OAuthAccount.cs new file mode 100644 index 0000000..a580cb3 --- /dev/null +++ b/src/DotNetOpenAuth.Web/OAuthAccount.cs @@ -0,0 +1,47 @@ +using System; +using System.Globalization; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web +{ + /// <summary> + /// Represents an OpenAuth & OpenID account. + /// </summary> + public class OAuthAccount + { + /// <summary> + /// Gets the provider name. + /// </summary> + public string Provider { get; private set; } + + /// <summary> + /// Gets the provider user id. + /// </summary> + public string ProviderUserId { get; private set; } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthAccountData"/> class. + /// </summary> + /// <param name="provider">The provider.</param> + /// <param name="providerUserId">The provider user id.</param> + public OAuthAccount(string provider, string providerUserId) + { + if (string.IsNullOrEmpty(provider)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "provider"), + "provider"); + } + + if (string.IsNullOrEmpty(providerUserId)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "providerUserId"), + "providerUserId"); + } + + Provider = provider; + ProviderUserId = providerUserId; + } + } +} diff --git a/src/DotNetOpenAuth.Web/OAuthAuthenticationTicketHelper.cs b/src/DotNetOpenAuth.Web/OAuthAuthenticationTicketHelper.cs new file mode 100644 index 0000000..ba351d0 --- /dev/null +++ b/src/DotNetOpenAuth.Web/OAuthAuthenticationTicketHelper.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics; +using System.Web; +using System.Web.Security; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web +{ + internal static class OAuthAuthenticationTicketHelper + { + private const string OAuthCookieToken = "OAuth"; + + public static void SetAuthenticationTicket(HttpContextBase context, string userName, bool createPersistentCookie) + { + if (!context.Request.IsSecureConnection && FormsAuthentication.RequireSSL) + { + throw new HttpException(WebResources.ConnectionNotSecure); + } + + HttpCookie cookie = GetAuthCookie(userName, createPersistentCookie); + context.Response.Cookies.Add(cookie); + } + + public static bool IsOAuthAuthenticationTicket(HttpContextBase context) + { + HttpCookie cookie = context.Request.Cookies[FormsAuthentication.FormsCookieName]; + if (cookie == null) + { + return false; + } + + string encryptedCookieData = cookie.Value; + if (String.IsNullOrEmpty(encryptedCookieData)) + { + return false; + } + + try + { + FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(encryptedCookieData); + return authTicket != null && !authTicket.Expired && authTicket.UserData == OAuthCookieToken; + } + catch (ArgumentException) + { + return false; + } + } + + private static HttpCookie GetAuthCookie(string userName, bool createPersistentCookie) + { + Debug.Assert(!String.IsNullOrEmpty(userName)); + + var ticket = new FormsAuthenticationTicket( + /* version */ 2, + userName, + DateTime.Now, + DateTime.Now.Add(FormsAuthentication.Timeout), + createPersistentCookie, + OAuthCookieToken, + FormsAuthentication.FormsCookiePath); + + string encryptedTicket = FormsAuthentication.Encrypt(ticket); + if (encryptedTicket == null || encryptedTicket.Length < 1) + { + throw new HttpException(WebResources.FailedToEncryptTicket); + } + + var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) + { + HttpOnly = true, + Path = FormsAuthentication.FormsCookiePath, + Secure = FormsAuthentication.RequireSSL + }; + + if (FormsAuthentication.CookieDomain != null) + { + cookie.Domain = FormsAuthentication.CookieDomain; + } + + if (ticket.IsPersistent) + { + cookie.Expires = ticket.Expiration; + } + + return cookie; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/OAuthWebSecurity.cs b/src/DotNetOpenAuth.Web/OAuthWebSecurity.cs new file mode 100644 index 0000000..7a3f864 --- /dev/null +++ b/src/DotNetOpenAuth.Web/OAuthWebSecurity.cs @@ -0,0 +1,386 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Threading; +using System.Web; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.Web.Clients; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web +{ + /// <summary> + /// Contains APIs to manage authentication against OAuth & OpenID service providers + /// </summary> + public static class OAuthWebSecurity + { + private const string ProviderQueryStringName = "__provider__"; + + private static IOAuthDataProvider _oAuthDataProvider; + private static IOAuthDataProvider OAuthDataProvider + { + get + { + return _oAuthDataProvider; + } + } + + // contains all registered authentication clients + private static readonly AuthenticationClientCollection _authenticationClients = new AuthenticationClientCollection(); + + public static void RegisterDataProvider(IOAuthDataProvider dataProvider) + { + if (dataProvider == null) + { + throw new ArgumentNullException("dataProvider"); + } + + var originalValue = Interlocked.CompareExchange(ref _oAuthDataProvider, dataProvider, null); + if (originalValue != null) + { + throw new InvalidOperationException(WebResources.OAuthDataProviderRegistered); + } + } + + public static bool IsOAuthDataProviderRegistered + { + get + { + return OAuthDataProvider != null; + } + } + + private static void EnsureDataProvider() + { + if (!IsOAuthDataProviderRegistered) + { + throw new InvalidOperationException(WebResources.OAuthDataProviderNotRegistered); + } + } + + /// <summary> + /// Registers a supported OAuth client with the specified consumer key and consumer secret. + /// </summary> + /// <param name="client">One of the supported OAuth clients.</param> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + public static void RegisterOAuthClient(BuiltInOAuthClient client, string consumerKey, string consumerSecret) + { + IAuthenticationClient authenticationClient; + switch (client) + { + case BuiltInOAuthClient.LinkedIn: + authenticationClient = new LinkedInClient(consumerKey, consumerSecret); + break; + + case BuiltInOAuthClient.Twitter: + authenticationClient = new TwitterClient(consumerKey, consumerSecret); + break; + + case BuiltInOAuthClient.Facebook: + authenticationClient = new FacebookClient(consumerKey, consumerSecret); + break; + + case BuiltInOAuthClient.WindowsLive: + authenticationClient = new WindowsLiveClient(consumerKey, consumerSecret); + break; + + default: + throw new ArgumentOutOfRangeException("client"); + } + RegisterClient(authenticationClient); + } + + /// <summary> + /// Registers a supported OpenID client + /// </summary> + public static void RegisterOpenIDClient(BuiltInOpenIDClient openIDClient) + { + IAuthenticationClient client; + switch (openIDClient) + { + case BuiltInOpenIDClient.Google: + client = new GoogleOpenIdClient(); + break; + + case BuiltInOpenIDClient.Yahoo: + client = new YahooOpenIdClient(); + break; + + default: + throw new ArgumentOutOfRangeException("openIDClient"); + } + + RegisterClient(client); + } + + /// <summary> + /// Registers an authentication client. + /// </summary> + public static void RegisterClient(IAuthenticationClient client) + { + if (client == null) + { + throw new ArgumentNullException("client"); + } + + if (String.IsNullOrEmpty(client.ProviderName)) + { + throw new ArgumentException(WebResources.InvalidServiceProviderName, "client"); + } + + if (_authenticationClients.Contains(client)) + { + throw new ArgumentException(WebResources.ServiceProviderNameExists, "client"); + } + + _authenticationClients.Add(client); + } + + /// <summary> + /// Requests the specified provider to start the authentication by directing users to an external website + /// </summary> + /// <param name="provider">The provider.</param> + public static void RequestAuthentication(string provider) + { + RequestAuthentication(provider, returnUrl: null); + } + + /// <summary> + /// Requests the specified provider to start the authentication by directing users to an external website + /// </summary> + /// <param name="provider">The provider.</param> + /// <param name="returnUrl">The return url after user is authenticated.</param> + [SuppressMessage( + "Microsoft.Design", + "CA1054:UriParametersShouldNotBeStrings", + MessageId = "1#", + Justification = "We want to allow relative app path, and support ~/")] + public static void RequestAuthentication(string provider, string returnUrl) + { + if (HttpContext.Current == null) + { + throw new InvalidOperationException(WebResources.HttpContextNotAvailable); + } + + RequestAuthenticationCore(new HttpContextWrapper(HttpContext.Current), provider, returnUrl); + } + + internal static void RequestAuthenticationCore(HttpContextBase context, string provider, string returnUrl) + { + if (String.IsNullOrEmpty(provider)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "provider"), + "provider"); + } + + IAuthenticationClient client = GetOAuthClient(provider); + + // convert returnUrl to an absolute path + Uri uri; + if (!String.IsNullOrEmpty(returnUrl)) + { + uri = UriHelper.ConvertToAbsoluteUri(returnUrl); + } + else + { + uri = UriHelper.GetPublicFacingUrl(context.Request); + } + // attach the provider parameter so that we know which provider initiated + // the login when user is redirected back to this page + uri = uri.AttachQueryStringParameter(ProviderQueryStringName, provider); + client.RequestAuthentication(context, uri); + } + + /// <summary> + /// Checks if user is successfully authenticated when user is redirected back to this user. + /// </summary> + /// <returns></returns> + public static AuthenticationResult VerifyAuthentication() + { + if (HttpContext.Current == null) + { + throw new InvalidOperationException(WebResources.HttpContextNotAvailable); + } + + return VerifyAuthenticationCore(new HttpContextWrapper(HttpContext.Current)); + } + + internal static AuthenticationResult VerifyAuthenticationCore(HttpContextBase context) + { + string providerName = context.Request.QueryString[ProviderQueryStringName]; + if (String.IsNullOrEmpty(providerName)) + { + return AuthenticationResult.Failed; + } + + IAuthenticationClient client; + if (TryGetOAuthClient(providerName, out client)) + { + AuthenticationResult result = client.VerifyAuthentication(context); + if (!result.IsSuccessful) + { + // if the result is a Failed result, creates a new Failed response which has providerName info. + result = new AuthenticationResult(isSuccessful: false, provider: providerName, providerUserId: null, + userName: null, extraData: null); + } + + return result; + } + else + { + throw new InvalidOperationException(WebResources.InvalidServiceProviderName); + } + } + + /// <summary> + /// Checks if the specified provider user id represents a valid account. + /// If it does, log user in. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="providerUserId">The provider user id.</param> + /// <returns><c>true</c> if the login is successful.</returns> + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "Login is used more consistently in ASP.Net")] + public static bool Login(string providerName, string providerUserId, bool createPersistentCookie) + { + if (HttpContext.Current == null) + { + throw new InvalidOperationException(WebResources.HttpContextNotAvailable); + } + + return LoginCore(new HttpContextWrapper(HttpContext.Current), providerName, providerUserId, createPersistentCookie); + } + + internal static bool LoginCore(HttpContextBase context, string providerName, string providerUserId, bool createPersistentCookie) + { + EnsureDataProvider(); + + string userName = OAuthDataProvider.GetUserNameFromOAuth(providerName, providerUserId); + if (String.IsNullOrEmpty(userName)) + { + return false; + } + + OAuthAuthenticationTicketHelper.SetAuthenticationTicket( + context, + userName, + createPersistentCookie); + return true; + } + + /// <summary> + /// Gets a value indicating whether the current user is authenticated by an OAuth provider. + /// </summary> + public static bool IsAuthenticatedViaOAuth + { + get + { + if (HttpContext.Current == null) + { + throw new InvalidOperationException(WebResources.HttpContextNotAvailable); + } + + return GetIsAuthenticatedViaOAuthCore(new HttpContextWrapper(HttpContext.Current)); + } + } + + internal static bool GetIsAuthenticatedViaOAuthCore(HttpContextBase context) + { + if (!context.Request.IsAuthenticated) + { + return false; + } + return OAuthAuthenticationTicketHelper.IsOAuthAuthenticationTicket(context); + } + + /// <summary> + /// Creates or update the account with the specified provider, provider user id and associate it with the specified user name. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="providerUserId">The provider user id.</param> + /// <param name="userName">The user name.</param> + public static void CreateOrUpdateAccount(string providerName, string providerUserId, string userName) + { + EnsureDataProvider(); + OAuthDataProvider.CreateOrUpdateOAuthAccount(providerName, providerUserId, userName); + } + + public static string GetUsername(string providerName, string providerUserId) + { + EnsureDataProvider(); + return OAuthDataProvider.GetUserNameFromOAuth(providerName, providerUserId); + } + + /// <summary> + /// Gets all OAuth & OpenID accounts which are associted with the specified user name. + /// </summary> + /// <param name="userName">The user name.</param> + public static ICollection<OAuthAccount> GetAccountsFromUserName(string userName) + { + if (String.IsNullOrEmpty(userName)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "userName"), + "userName"); + } + + EnsureDataProvider(); + + return OAuthDataProvider.GetOAuthAccountsFromUserName(userName); + } + + /// <summary> + /// Delete the specified OAuth & OpenID account + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="providerUserId">The provider user id.</param> + public static void DeleteAccount(string providerName, string providerUserId) + { + EnsureDataProvider(); + + OAuthDataProvider.DeleteOAuthAccount(providerName, providerUserId); + } + + internal static IAuthenticationClient GetOAuthClient(string providerName) + { + if (!_authenticationClients.Contains(providerName)) + { + throw new ArgumentException(WebResources.ServiceProviderNotFound, "providerName"); + } + + return _authenticationClients[providerName]; + } + + internal static bool TryGetOAuthClient(string provider, out IAuthenticationClient client) + { + if (_authenticationClients.Contains(provider)) + { + client = _authenticationClients[provider]; + return true; + } + else + { + client = null; + return false; + } + } + + /// <summary> + /// for unit tests + /// </summary> + internal static void ClearProviders() + { + _authenticationClients.Clear(); + } + + /// <summary> + /// for unit tests + /// </summary> + internal static void ClearDataProvider() + { + _oAuthDataProvider = null; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.Web/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..039cc1e --- /dev/null +++ b/src/DotNetOpenAuth.Web/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.Web")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("DotNetOpenAuth.Web")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2011")] +[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("c89b7e57-2735-4407-bcb9-dfe9bb9493a2")] + +// 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: AssemblyFileVersion("1.0.0.0")] +[assembly: InternalsVisibleTo("DotnetOpenAuth.Web.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")]
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Resources/WebResources.Designer.cs b/src/DotNetOpenAuth.Web/Resources/WebResources.Designer.cs new file mode 100644 index 0000000..e35278d --- /dev/null +++ b/src/DotNetOpenAuth.Web/Resources/WebResources.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.488 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.Web.Resources { + 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 WebResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal WebResources() { + } + + /// <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.Web.Resources.WebResources", typeof(WebResources).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 {0} cannot be null or an empty string.. + /// </summary> + internal static string Argument_Cannot_Be_Null_Or_Empty { + get { + return ResourceManager.GetString("Argument_Cannot_Be_Null_Or_Empty", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A setting in web.config requires a secure connection for this request but the current connection is not secured.. + /// </summary> + internal static string ConnectionNotSecure { + get { + return ResourceManager.GetString("ConnectionNotSecure", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unable to encrypt the authentication ticket.. + /// </summary> + internal static string FailedToEncryptTicket { + get { + return ResourceManager.GetString("FailedToEncryptTicket", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to HttpContext is not available in the current thread.. + /// </summary> + internal static string HttpContextNotAvailable { + get { + return ResourceManager.GetString("HttpContextNotAvailable", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Invalid provider name.. + /// </summary> + internal static string InvalidServiceProviderName { + get { + return ResourceManager.GetString("InvalidServiceProviderName", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An OAuth data provider has not been registered for this application.. + /// </summary> + internal static string OAuthDataProviderNotRegistered { + get { + return ResourceManager.GetString("OAuthDataProviderNotRegistered", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An OAuth data provider has already been registered for this application.. + /// </summary> + internal static string OAuthDataProviderRegistered { + get { + return ResourceManager.GetString("OAuthDataProviderRegistered", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failed to obtain the authentication response from service provider.. + /// </summary> + internal static string OpenIDFailedToGetResponse { + get { + return ResourceManager.GetString("OpenIDFailedToGetResponse", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Invalid identifier. + /// </summary> + internal static string OpenIDInvalidIdentifier { + get { + return ResourceManager.GetString("OpenIDInvalidIdentifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Another service provider with the same name has already been registered.. + /// </summary> + internal static string ServiceProviderNameExists { + get { + return ResourceManager.GetString("ServiceProviderNameExists", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A service provider could not be found by the specified name.. + /// </summary> + internal static string ServiceProviderNotFound { + get { + return ResourceManager.GetString("ServiceProviderNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Couldn't find a username with the provided credentials.. + /// </summary> + internal static string UsernameNotFoundFromId { + get { + return ResourceManager.GetString("UsernameNotFoundFromId", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.Web/Resources/WebResources.resx b/src/DotNetOpenAuth.Web/Resources/WebResources.resx new file mode 100644 index 0000000..a4c0d5d --- /dev/null +++ b/src/DotNetOpenAuth.Web/Resources/WebResources.resx @@ -0,0 +1,156 @@ +<?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=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="Argument_Cannot_Be_Null_Or_Empty" xml:space="preserve"> + <value>{0} cannot be null or an empty string.</value> + </data> + <data name="ConnectionNotSecure" xml:space="preserve"> + <value>A setting in web.config requires a secure connection for this request but the current connection is not secured.</value> + </data> + <data name="FailedToEncryptTicket" xml:space="preserve"> + <value>Unable to encrypt the authentication ticket.</value> + </data> + <data name="HttpContextNotAvailable" xml:space="preserve"> + <value>HttpContext is not available in the current thread.</value> + </data> + <data name="InvalidServiceProviderName" xml:space="preserve"> + <value>Invalid provider name.</value> + </data> + <data name="OAuthDataProviderNotRegistered" xml:space="preserve"> + <value>An OAuth data provider has not been registered for this application.</value> + </data> + <data name="OAuthDataProviderRegistered" xml:space="preserve"> + <value>An OAuth data provider has already been registered for this application.</value> + </data> + <data name="OpenIDFailedToGetResponse" xml:space="preserve"> + <value>Failed to obtain the authentication response from service provider.</value> + </data> + <data name="OpenIDInvalidIdentifier" xml:space="preserve"> + <value>Invalid identifier</value> + </data> + <data name="ServiceProviderNameExists" xml:space="preserve"> + <value>Another service provider with the same name has already been registered.</value> + </data> + <data name="ServiceProviderNotFound" xml:space="preserve"> + <value>A service provider could not be found by the specified name.</value> + </data> + <data name="UsernameNotFoundFromId" xml:space="preserve"> + <value>Couldn't find a username with the provided credentials.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.WebPages/DotNetOpenAuth.WebPages.csproj b/src/DotNetOpenAuth.WebPages/DotNetOpenAuth.WebPages.csproj new file mode 100644 index 0000000..01d6166 --- /dev/null +++ b/src/DotNetOpenAuth.WebPages/DotNetOpenAuth.WebPages.csproj @@ -0,0 +1,92 @@ +<?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> + <StyleCopEnabled>False</StyleCopEnabled> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProductVersion>8.0.30703</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth.WebPages</RootNamespace> + <AssemblyName>DotNetOpenAuth.WebPages</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>..\..\bin\v4.0\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>ExtendedDesignGuidelineRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>..\..\bin\v4.0\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.Product.props" /> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="System.Configuration" /> + <Reference Include="System.Core" /> + <Reference Include="System.Web" /> + <Reference Include="System.Web.ApplicationServices" /> + <Reference Include="System.Web.Razor, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\System.Web.Razor.dll</HintPath> + </Reference> + <Reference Include="System.Web.WebPages, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\System.Web.WebPages.dll</HintPath> + </Reference> + <Reference Include="System.Web.WebPages.Administration, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\System.Web.WebPages.Administration.dll</HintPath> + </Reference> + <Reference Include="System.Web.WebPages.Deployment, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\System.Web.WebPages.Deployment.dll</HintPath> + </Reference> + <Reference Include="System.Web.WebPages.Razor, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\System.Web.WebPages.Razor.dll</HintPath> + </Reference> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + <Reference Include="WebMatrix.Data, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\WebMatrix.Data.dll</HintPath> + </Reference> + <Reference Include="WebMatrix.WebData, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> + <SpecificVersion>False</SpecificVersion> + <HintPath>..\..\..\WPF\WebStackRuntime\Runtime\bin\Release\WebMatrix.WebData.dll</HintPath> + </Reference> + </ItemGroup> + <ItemGroup> + <Compile Include="PreApplicationStartCode.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="WebPagesOAuthDataProvider.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\DotNetOpenAuth.Web\DotNetOpenAuth.Web.csproj"> + <Project>{51835086-9611-4C53-819B-F2D5C9320873}</Project> + <Name>DotNetOpenAuth.Web</Name> + </ProjectReference> + </ItemGroup> + <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.WebPages/PreApplicationStartCode.cs b/src/DotNetOpenAuth.WebPages/PreApplicationStartCode.cs new file mode 100644 index 0000000..c96173a --- /dev/null +++ b/src/DotNetOpenAuth.WebPages/PreApplicationStartCode.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using System.Web.WebPages.Razor; +using DotNetOpenAuth.Web; + +namespace DotNetOpenAuth.WebPages +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class PreApplicationStartCode + { + public static void Start() + { + WebPageRazorHost.AddGlobalImport("DotNetOpenAuth.Web"); + OAuthWebSecurity.RegisterDataProvider(new WebPagesOAuthDataProvider()); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.WebPages/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.WebPages/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..96b3016 --- /dev/null +++ b/src/DotNetOpenAuth.WebPages/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using System.Web; + +// 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.WebPages")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("DotNetOpenAuth.WebPages")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2011")] +[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("c814e5db-c38b-4a8b-9a99-3b851c356ce5")] + +[assembly: AssemblyFileVersion("1.0.0.0")] + +[assembly: PreApplicationStartMethod(typeof(DotNetOpenAuth.WebPages.PreApplicationStartCode), "Start")]
\ No newline at end of file diff --git a/src/DotNetOpenAuth.WebPages/WebPagesOAuthDataProvider.cs b/src/DotNetOpenAuth.WebPages/WebPagesOAuthDataProvider.cs new file mode 100644 index 0000000..b8895c0 --- /dev/null +++ b/src/DotNetOpenAuth.WebPages/WebPagesOAuthDataProvider.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Security; +using DotNetOpenAuth.Web; +using WebMatrix.WebData; + +namespace DotNetOpenAuth.WebPages +{ + internal class WebPagesOAuthDataProvider : IOAuthDataProvider + { + public WebPagesOAuthDataProvider() + { + } + + private static ExtendedMembershipProvider VerifyProvider() + { + var provider = Membership.Provider as ExtendedMembershipProvider; + if (provider == null) + { + throw new InvalidOperationException(); + } + return provider; + } + + public string GetUserNameFromOAuth(string oAuthProvider, string oAuthId) + { + ExtendedMembershipProvider provider = VerifyProvider(); + + int userId = provider.GetUserIdFromOAuth(oAuthProvider, oAuthId); + if (userId == -1) { + return null; + } + + return provider.GetUserNameFromId(userId); + } + + public void CreateOrUpdateOAuthAccount(string oAuthProvider, string oAuthId, string username) + { + ExtendedMembershipProvider provider = VerifyProvider(); + provider.CreateOrUpdateOAuthAccount(oAuthProvider, oAuthId, username); + } + + public bool DeleteOAuthAccount(string oAuthProvider, string oAuthId) + { + ExtendedMembershipProvider provider = VerifyProvider(); + + string username = GetUserNameFromOAuth(oAuthProvider, oAuthId); + if (String.IsNullOrEmpty(username)) + { + // account doesn't exist + return false; + } + + provider.DeleteOAuthAccount(oAuthProvider, oAuthId); + return true; + } + + public ICollection<OAuthAccount> GetOAuthAccountsFromUserName(string userName) + { + ExtendedMembershipProvider provider = VerifyProvider(); + return provider.GetAccountsForUser(userName).Select(p => new OAuthAccount(p.Provider, p.ProviderUserId)).ToList(); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index 505ce7e..eae5e98 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -202,6 +202,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Combinations", "Combination EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.OpenIdInfoCard.UI", "DotNetOpenAuth.OpenIdInfoCard.UI\DotNetOpenAuth.OpenIdInfoCard.UI.csproj", "{3A8347E8-59A5-4092-8842-95C75D7D2F36}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.Web", "DotNetOpenAuth.Web\DotNetOpenAuth.Web.csproj", "{51835086-9611-4C53-819B-F2D5C9320873}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.WebPages", "DotNetOpenAuth.WebPages\DotNetOpenAuth.WebPages.csproj", "{AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.Web.Test", "DotNetOpenAuth.Web.Test\DotNetOpenAuth.Web.Test.csproj", "{C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeAnalysis|Any CPU = CodeAnalysis|Any CPU @@ -224,13 +230,6 @@ Global {AA78D112-D889-414B-A7D4-467B34C7B663}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA78D112-D889-414B-A7D4-467B34C7B663}.Release|Any CPU.Build.0 = Release|Any CPU {AA78D112-D889-414B-A7D4-467B34C7B663}.ReleaseNoUI|Any CPU.ActiveCfg = Release|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.Debug|Any CPU.Build.0 = Debug|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.Release|Any CPU.Build.0 = Debug|Any CPU - {47A84EF7-68C3-4D47-926A-9CCEA6518531}.ReleaseNoUI|Any CPU.ActiveCfg = Debug|Any CPU {2A59DE0A-B76A-4B42-9A33-04D34548353D}.CodeAnalysis|Any CPU.ActiveCfg = CodeAnalysis|Any CPU {2A59DE0A-B76A-4B42-9A33-04D34548353D}.CodeAnalysis|Any CPU.Build.0 = CodeAnalysis|Any CPU {2A59DE0A-B76A-4B42-9A33-04D34548353D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -245,13 +244,6 @@ Global {AEA29D4D-396F-47F6-BC81-B58D4B855245}.Release|Any CPU.ActiveCfg = Release|Any CPU {AEA29D4D-396F-47F6-BC81-B58D4B855245}.Release|Any CPU.Build.0 = Release|Any CPU {AEA29D4D-396F-47F6-BC81-B58D4B855245}.ReleaseNoUI|Any CPU.ActiveCfg = Release|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.Release|Any CPU.Build.0 = Debug|Any CPU - {6EB90284-BD15-461C-BBF2-131CF55F7C8B}.ReleaseNoUI|Any CPU.ActiveCfg = Debug|Any CPU {07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU {07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU {07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -529,6 +521,30 @@ Global {3A8347E8-59A5-4092-8842-95C75D7D2F36}.Release|Any CPU.Build.0 = Release|Any CPU {3A8347E8-59A5-4092-8842-95C75D7D2F36}.ReleaseNoUI|Any CPU.ActiveCfg = Release|Any CPU {3A8347E8-59A5-4092-8842-95C75D7D2F36}.ReleaseNoUI|Any CPU.Build.0 = Release|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.Release|Any CPU.Build.0 = Release|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.ReleaseNoUI|Any CPU.ActiveCfg = Release|Any CPU + {51835086-9611-4C53-819B-F2D5C9320873}.ReleaseNoUI|Any CPU.Build.0 = Release|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.Release|Any CPU.Build.0 = Release|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.ReleaseNoUI|Any CPU.ActiveCfg = Release|Any CPU + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8}.ReleaseNoUI|Any CPU.Build.0 = Release|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.Release|Any CPU.Build.0 = Release|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.ReleaseNoUI|Any CPU.ActiveCfg = Release|Any CPU + {C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}.ReleaseNoUI|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -551,7 +567,6 @@ Global {9529606E-AF76-4387-BFB7-3D10A5B399AA} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} {E135F455-0669-49F8-9207-07FCA8C8FC79} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} {C78E8235-1D46-43EB-A912-80B522C4E9AE} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} - {6EB90284-BD15-461C-BBF2-131CF55F7C8B} = {8A5CEDB9-7F8A-4BE2-A1B9-97130F453277} {5C65603B-235F-47E6-B536-06385C60DE7F} = {E9ED920D-1F83-48C0-9A4B-09CCE505FE6D} {A78F8FC6-7B03-4230-BE41-761E400D6810} = {B9EB8729-4B54-4453-B089-FE6761BA3057} {17932639-1F50-48AF-B0A5-E2BF832F82CC} = {B9EB8729-4B54-4453-B089-FE6761BA3057} @@ -564,6 +579,8 @@ Global {173E7B8D-E751-46E2-A133-F72297C0D2F4} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} {60426312-6AE5-4835-8667-37EDEA670222} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} {57A7DD35-666C-4FA3-9A1B-38961E50CA27} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} + {51835086-9611-4C53-819B-F2D5C9320873} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} + {AE031B0C-710B-4D5B-BDF5-F21DFC9092D8} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} {F8284738-3B5D-4733-A511-38C23F4A763F} = {C7EF1823-3AA7-477E-8476-28929F5C05D2} {F458AB60-BA1C-43D9-8CEF-EC01B50BE87B} = {C7EF1823-3AA7-477E-8476-28929F5C05D2} {F4CD3C04-6037-4946-B7A5-34BFC96A75D2} = {C7EF1823-3AA7-477E-8476-28929F5C05D2} |