diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-03-01 19:47:05 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-03-01 19:47:05 -0800 |
commit | 6d18e14045cc9a8c0926e037ede82fc2d7818079 (patch) | |
tree | 07a9ba5991e2bd09b6cdf4a3e53dfd295c017a75 | |
parent | 3ed1a19d2ba373869e5d1aa285726f3b5b99d0c2 (diff) | |
parent | 2137afe38f2c403892d479bf1761d40f444ff141 (diff) | |
download | DotNetOpenAuth-6d18e14045cc9a8c0926e037ede82fc2d7818079.zip DotNetOpenAuth-6d18e14045cc9a8c0926e037ede82fc2d7818079.tar.gz DotNetOpenAuth-6d18e14045cc9a8c0926e037ede82fc2d7818079.tar.bz2 |
Merge branch 'webpages'
42 files changed, 2742 insertions, 5 deletions
diff --git a/nuget/DotNetOpenAuth.AspNet.nuspec b/nuget/DotNetOpenAuth.AspNet.nuspec new file mode 100644 index 0000000..c518176 --- /dev/null +++ b/nuget/DotNetOpenAuth.AspNet.nuspec @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> + <metadata> + <id>DotNetOpenAuth.AspNet</id> + <version>$version$</version> + <title>DotNetOpenAuth extensions for ASP.NET (WebPages)</title> + <authors>Luan Nguyen</authors> + <owners>Outercurve Foundation</owners> + <projectUrl>http://www.dotnetopenauth.net/</projectUrl> + <iconUrl>https://github.com/AArnott/dotnetopenid/raw/v3.4/doc/logo/dnoa-logo_32x32.png</iconUrl> + <licenseUrl>http://www.opensource.org/licenses/ms-pl.html</licenseUrl> + <requireLicenseAcceptance>false</requireLicenseAcceptance> + <description>Allows adding OAuth and OpenID authentication to ASP.NET web projects using DotNetOpenAuth implementation.</description> + <dependencies> + <dependency id="DotNetOpenAuth.OpenId.RelyingParty" version="[$version$]" /> + <dependency id="DotNetOpenAuth.OAuth.Consumer" version="[$version$]" /> + </dependencies> + </metadata> + <files> + <file src="$OutputPath40$signed\DotNetOpenAuth.AspNet.dll" target="lib\net40-full\DotNetOpenAuth.AspNet.dll" /> + <file src="$OutputPath40$DotNetOpenAuth.AspNet.pdb" target="lib\net40-full\DotNetOpenAuth.AspNet.pdb" /> + <file src="$OutputPath40$DotNetOpenAuth.AspNet.xml" target="lib\net40-full\DotNetOpenAuth.AspNet.xml" /> + + <file src="..\src\DotNetOpenAuth.AspNet\**\*.cs" target="src" /> + </files> +</package>
\ No newline at end of file diff --git a/nuget/nuget.proj b/nuget/nuget.proj index bf86b1c..faf4bae 100644 --- a/nuget/nuget.proj +++ b/nuget/nuget.proj @@ -7,6 +7,8 @@ <ItemGroup> <ProductTargets Include="BuildUnifiedProduct;ReSignDelaySignedAssemblies" Condition=" '$(SkipNugetDependenciesBuild)' != 'true' " /> <ProductTargets Include="GetOutputPath" /> + <AspNetTargets Include="Build;Sign" Condition=" '$(SkipNugetDependenciesBuild)' != 'true' " /> + <AspNetTargets Include="GetOutputPath" /> </ItemGroup> <!-- We build the entire unified, signed product targeting both CLRs, since NuGet supports packages that contain both, @@ -25,6 +27,13 @@ BuildInParallel="$(BuildInParallel)"> <Output TaskParameter="TargetOutputs" ItemName="TargetOutputs40"/> </MSBuild> + <MSBuild + Projects="$(ProjectRoot)src\DotNetOpenAuth.AspNet\DotNetOpenAuth.AspNet.csproj" + Targets="@(AspNetTargets)" + Properties="TargetFrameworkVersion=v4.0" + BuildInParallel="$(BuildInParallel)"> + <Output TaskParameter="TargetOutputs" ItemName="TargetOutputs40"/> + </MSBuild> <ItemGroup> <ResignedAssembliesOutputs Include="@(TargetOutputs35)" Condition=" '%(MSBuildSourceTargetName)' == 'Sign' "> diff --git a/src/DotNetOpenAuth.AspNet.Test/DotNetOpenAuth.AspNet.Test.csproj b/src/DotNetOpenAuth.AspNet.Test/DotNetOpenAuth.AspNet.Test.csproj new file mode 100644 index 0000000..dd7228f --- /dev/null +++ b/src/DotNetOpenAuth.AspNet.Test/DotNetOpenAuth.AspNet.Test.csproj @@ -0,0 +1,79 @@ +<?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.AspNet.Test</RootNamespace> + <AssemblyName>DotNetOpenAuth.AspNet.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="Properties\AssemblyInfo.cs" /> + <Compile Include="UriHelperTest.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\DotNetOpenAuth.Core\DotNetOpenAuth.Core.csproj"> + <Project>{60426312-6AE5-4835-8667-37EDEA670222}</Project> + <Name>DotNetOpenAuth.Core</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OAuth\DotNetOpenAuth.OAuth.csproj"> + <Project>{A288FCC8-6FCF-46DA-A45E-5F9281556361}</Project> + <Name>DotNetOpenAuth.OAuth</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.AspNet\DotNetOpenAuth.AspNet.csproj"> + <Project>{51835086-9611-4C53-819B-F2D5C9320873}</Project> + <Name>DotNetOpenAuth.AspNet</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.AspNet.Test/OAuth2ClientTest.cs b/src/DotNetOpenAuth.AspNet.Test/OAuth2ClientTest.cs new file mode 100644 index 0000000..d24cf77 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet.Test/OAuth2ClientTest.cs @@ -0,0 +1,127 @@ +namespace DotNetOpenAuth.AspNet.Test { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Web; + using DotNetOpenAuth.AspNet.Clients; + using Moq; + using NUnit.Framework; + + [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.AspNet.Test/OAuthAuthenticationTickerHelperTest.cs b/src/DotNetOpenAuth.AspNet.Test/OAuthAuthenticationTickerHelperTest.cs new file mode 100644 index 0000000..78b8ca6 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet.Test/OAuthAuthenticationTickerHelperTest.cs @@ -0,0 +1,143 @@ +namespace DotNetOpenAuth.Test.Web { + using System; + using System.Web; + using System.Web.Security; + using DotNetOpenAuth.AspNet; + using Moq; + using NUnit.Framework; + + [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 = OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(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 = OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(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 = OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(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 = OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(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 + OpenAuthAuthenticationTicketHelper.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.AspNet.Test/OAuthClientTest.cs b/src/DotNetOpenAuth.AspNet.Test/OAuthClientTest.cs new file mode 100644 index 0000000..bec7f8a --- /dev/null +++ b/src/DotNetOpenAuth.AspNet.Test/OAuthClientTest.cs @@ -0,0 +1,130 @@ +namespace DotNetOpenAuth.AspNet.Test { + using System; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.AspNet.Clients; + using Moq; + using NUnit.Framework; + using DotNetOpenAuth.AspNet; + + [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.AspNet.Test/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.AspNet.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3c9f4f6 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet.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.AspNet.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft IT")] +[assembly: AssemblyProduct("DotNetOpenAuth.AspNet.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.AspNet.Test/UriHelperTest.cs b/src/DotNetOpenAuth.AspNet.Test/UriHelperTest.cs new file mode 100644 index 0000000..ea78a76 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet.Test/UriHelperTest.cs @@ -0,0 +1,37 @@ +namespace DotNetOpenAuth.AspNet.Test { + using System; + using DotNetOpenAuth.AspNet.Clients; + using NUnit.Framework; + + [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()); + } + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs b/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs new file mode 100644 index 0000000..9656a56 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs @@ -0,0 +1,102 @@ +namespace DotNetOpenAuth.AspNet { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <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; } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="isSuccessful">if set to <c>true</c> [is successful].</param> + public AuthenticationResult(bool isSuccessful) : + this(isSuccessful, + provider: null, + providerUserId: null, + userName: null, + extraData: null) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="exception">The exception.</param> + public AuthenticationResult(Exception exception) + : this(isSuccessful: false) { + if (exception == null) { + throw new ArgumentNullException("exception"); + } + + Error = exception; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="isSuccessful">if set to <c>true</c> [is successful].</param> + /// <param name="provider">The provider.</param> + /// <param name="providerUserId">The provider user id.</param> + /// <param name="userName">Name of the user.</param> + /// <param name="extraData">The extra data.</param> + public AuthenticationResult( + bool isSuccessful, + string provider, + string providerUserId, + string userName, + IDictionary<string, string> extraData) { + IsSuccessful = isSuccessful; + Provider = provider; + ProviderUserId = providerUserId; + UserName = userName; + if (extraData != null) { + // wrap extraData in a read-only dictionary + ExtraData = new ReadOnlyDictionary<string, string>(extraData); + } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/DictionaryExtensions.cs b/src/DotNetOpenAuth.AspNet/Clients/DictionaryExtensions.cs new file mode 100644 index 0000000..ce7891d --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/DictionaryExtensions.cs @@ -0,0 +1,39 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Xml.Linq; + + 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.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs new file mode 100644 index 0000000..5ad709c --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs @@ -0,0 +1,39 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Net; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + 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.AspNet/Clients/OAuth/IOAuthWebWorker.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/IOAuthWebWorker.cs new file mode 100644 index 0000000..cca8298 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/IOAuthWebWorker.cs @@ -0,0 +1,12 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Net; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + + 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.AspNet/Clients/OAuth/InMemoryOAuthTokenManager.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/InMemoryOAuthTokenManager.cs new file mode 100644 index 0000000..9dbf3d8 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/InMemoryOAuthTokenManager.cs @@ -0,0 +1,117 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <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.AspNet/Clients/OAuth/LinkedInClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs new file mode 100644 index 0000000..53578ab --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs @@ -0,0 +1,85 @@ +namespace DotNetOpenAuth.AspNet.Clients { + 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; + + /// <summary> + /// Represents LinkedIn authentication client. + /// </summary> + public 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.AspNet/Clients/OAuth/OAuthClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/OAuthClient.cs new file mode 100644 index 0000000..5e57f6e --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/OAuthClient.cs @@ -0,0 +1,109 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <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; + } + + // add the access token to the user data dictionary just in case page developers want to use it + AuthenticationResult result = VerifyAuthenticationCore(response); + if (result.IsSuccessful && result.ExtraData != null) + { + result.ExtraData["accesstoken"] = response.AccessToken; + } + + return result; + } + + /// <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.AspNet/Clients/OAuth/TwitterClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs new file mode 100644 index 0000000..c73deed --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs @@ -0,0 +1,84 @@ +namespace DotNetOpenAuth.AspNet.Clients { + 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; + + /// <summary> + /// Represents a Twitter client + /// </summary> + public 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.AspNet/Clients/OAuth2/FacebookClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs new file mode 100644 index 0000000..623d595 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs @@ -0,0 +1,91 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Net; + using System.Web; + using DotNetOpenAuth.AspNet.Resources; + using DotNetOpenAuth.Messaging; + + public 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); + MessagingUtilities.AppendQueryArgs(builder, + new KeyValuePair<string, string>[] { + new KeyValuePair<string, string>("client_id", _appId), + new KeyValuePair<string, string>("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); + MessagingUtilities.AppendQueryArgs(builder, + new KeyValuePair<string, string>[] { + new KeyValuePair<string, string>("client_id", _appId), + new KeyValuePair<string, string>("redirect_uri", returnUrl.ToString()), + new KeyValuePair<string, string>("client_secret", _appSecret), + new KeyValuePair<string, string>("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) { + FacebookGraphData graphData; + var request = WebRequest.Create("https://graph.facebook.com/me?access_token=" + Uri.EscapeDataString(accessToken)); + using (var response = request.GetResponse()) { + using (var responseStream = response.GetResponseStream()) { + graphData = JsonHelper.Deserialize<FacebookGraphData>(responseStream); + } + } + + // this dictionary must contains + var userData = new Dictionary<string, string>(); + userData.AddItemIfNotEmpty("id", graphData.Id); + userData.AddItemIfNotEmpty("username", graphData.Email); + userData.AddItemIfNotEmpty("name", graphData.Name); + userData.AddItemIfNotEmpty("link", graphData.Link == null ? null : graphData.Link.ToString()); + userData.AddItemIfNotEmpty("gender", graphData.Gender); + userData.AddItemIfNotEmpty("birthday", graphData.Birthday); + return userData; + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookGraphData.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookGraphData.cs new file mode 100644 index 0000000..a2605bf --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookGraphData.cs @@ -0,0 +1,34 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Runtime.Serialization; + using System.ComponentModel; + + /// <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] + [EditorBrowsable(EditorBrowsableState.Never)] + public class FacebookGraphData { + [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.AspNet/Clients/OAuth2/JsonHelper.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/JsonHelper.cs new file mode 100644 index 0000000..bc8af46 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/JsonHelper.cs @@ -0,0 +1,16 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.IO; + using System.Runtime.Serialization.Json; + + 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.AspNet/Clients/OAuth2/OAuth2AccessTokenData.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2AccessTokenData.cs new file mode 100644 index 0000000..7cb902e --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2AccessTokenData.cs @@ -0,0 +1,18 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System.Runtime.Serialization; + + [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.AspNet/Clients/OAuth2/OAuth2Client.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs new file mode 100644 index 0000000..ff50521 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs @@ -0,0 +1,122 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Web; + + /// <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; + } + + // add the access token to the user data dictionary just in case page developers want to use it + userData["accesstoken"] = accessToken; + + 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.AspNet/Clients/OAuth2/WindowsLiveClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveClient.cs new file mode 100644 index 0000000..bddb801 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveClient.cs @@ -0,0 +1,95 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using System.Text; + using DotNetOpenAuth.Messaging; + + public 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); + MessagingUtilities.AppendQueryArgs(builder, + new KeyValuePair<string, string>[] + { + new KeyValuePair<string, string>("client_id", _appId), + new KeyValuePair<string, string>("scope", "wl.basic"), + new KeyValuePair<string, string>("response_type", "code"), + new KeyValuePair<string, string>("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.AspNet/Clients/OAuth2/WindowsLiveUserData.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveUserData.cs new file mode 100644 index 0000000..8147e2f --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveUserData.cs @@ -0,0 +1,34 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Runtime.Serialization; + using System.ComponentModel; + + /// <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] + [EditorBrowsable(EditorBrowsableState.Never)] + 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.AspNet/Clients/OpenID/GoogleOpenIdClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs new file mode 100644 index 0000000..654fff6 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs @@ -0,0 +1,49 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System.Collections.Generic; + using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Represents Google OpenID client. + /// </summary> + public 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(WellKnownAttributes.Name.First, isRequired: false)); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Name.Last, 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(WellKnownAttributes.Name.First)); + extraData.AddItemIfNotEmpty("lastName", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Last)); + + return extraData; + } + + return null; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Clients/OpenID/OpenIDClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/OpenIDClient.cs new file mode 100644 index 0000000..4bfb87e --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/OpenIDClient.cs @@ -0,0 +1,127 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Web; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + using DotNetOpenAuth.AspNet.Resources; + + /// <summary> + /// Base classes for OpenID clients. + /// </summary> + public 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="context">The context of the current request.</param> + /// <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> + /// <param name="context">The context of the current request.</param> + /// <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.AspNet/Clients/OpenID/YahooOpenIdClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/YahooOpenIdClient.cs new file mode 100644 index 0000000..dcf6a13 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/YahooOpenIdClient.cs @@ -0,0 +1,42 @@ +namespace DotNetOpenAuth.AspNet.Clients { + using System.Collections.Generic; + using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; + using DotNetOpenAuth.OpenId.RelyingParty; + + public 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(WellKnownAttributes.Name.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(WellKnownAttributes.Name.FullName)); + + return extraData; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj new file mode 100644 index 0000000..e27febc --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj @@ -0,0 +1,105 @@ +<?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> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{51835086-9611-4C53-819B-F2D5C9320873}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth.AspNet</RootNamespace> + <AssemblyName>DotNetOpenAuth.AspNet</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <CodeAnalysisRuleSet>ExtendedDesignGuidelineRules.ruleset</CodeAnalysisRuleSet> + </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> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>..\..\bin\v4.0\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + </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="Clients\DictionaryExtensions.cs" /> + <Compile Include="IAuthenticationClient.cs" /> + <Compile Include="Clients\OAuth2\FacebookClient.cs" /> + <Compile Include="Clients\OAuth2\FacebookGraphData.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\GoogleOpenIdClient.cs" /> + <Compile Include="Clients\OpenID\OpenIDClient.cs" /> + <Compile Include="Clients\OpenID\YahooOpenIdClient.cs" /> + <Compile Include="UriHelper.cs" /> + <Compile Include="IOpenAuthDataProvider.cs" /> + <Compile Include="OpenAuthAuthenticationTicketHelper.cs" /> + <Compile Include="OpenAuthSecurityManager.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.Core\DotNetOpenAuth.Core.csproj"> + <Project>{60426312-6AE5-4835-8667-37EDEA670222}</Project> + <Name>DotNetOpenAuth.Core</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.AspNet/IAuthenticationClient.cs b/src/DotNetOpenAuth.AspNet/IAuthenticationClient.cs new file mode 100644 index 0000000..b5745c3 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/IAuthenticationClient.cs @@ -0,0 +1,31 @@ +namespace DotNetOpenAuth.AspNet { + using System; + using System.Web; + + /// <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.AspNet/IOpenAuthDataProvider.cs b/src/DotNetOpenAuth.AspNet/IOpenAuthDataProvider.cs new file mode 100644 index 0000000..5eb19a5 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/IOpenAuthDataProvider.cs @@ -0,0 +1,5 @@ +namespace DotNetOpenAuth.AspNet { + public interface IOpenAuthDataProvider { + string GetUserNameFromOpenAuth(string openAuthProvider, string openAuthId); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs b/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs new file mode 100644 index 0000000..22c1556 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs @@ -0,0 +1,73 @@ +namespace DotNetOpenAuth.AspNet { + using System; + using System.Diagnostics; + using System.Web; + using System.Web.Security; + using DotNetOpenAuth.AspNet.Resources; + + internal static class OpenAuthAuthenticationTicketHelper { + private const string OpenAuthCookieToken = "OpenAuth"; + + 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 IsValidAuthenticationTicket(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 == OpenAuthCookieToken; + } 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, + OpenAuthCookieToken, + 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.AspNet/OpenAuthSecurityManager.cs b/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs new file mode 100644 index 0000000..188307f --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs @@ -0,0 +1,112 @@ +namespace DotNetOpenAuth.AspNet { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Web; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Manage authenticating with an external OAuth or OpenID provider + /// </summary> + public class OpenAuthSecurityManager { + private const string ProviderQueryStringName = "__provider__"; + + private readonly HttpContextBase _requestContext; + private readonly IOpenAuthDataProvider _dataProvider; + private readonly IAuthenticationClient _authenticationProvider; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenAuthSecurityManager"/> class. + /// </summary> + /// <param name="requestContext">The request context.</param> + public OpenAuthSecurityManager(HttpContextBase requestContext) : + this(requestContext, provider: null, dataProvider: null) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenAuthSecurityManager"/> class. + /// </summary> + /// <param name="requestContext">The request context.</param> + /// <param name="provider">The provider.</param> + /// <param name="dataProvider">The data provider.</param> + public OpenAuthSecurityManager(HttpContextBase requestContext, IAuthenticationClient provider, IOpenAuthDataProvider dataProvider) { + if (requestContext == null) { + throw new ArgumentNullException("requestContext"); + } + + _requestContext = requestContext; + _dataProvider = dataProvider; + _authenticationProvider = provider; + } + + /// <summary> + /// Requests the specified provider to start the authentication by directing users to an external website + /// </summary> + /// <param name="returnUrl">The return url after user is authenticated.</param> + public void RequestAuthentication(string returnUrl) { + // convert returnUrl to an absolute path + Uri uri; + if (!String.IsNullOrEmpty(returnUrl)) { + uri = UriHelper.ConvertToAbsoluteUri(returnUrl, _requestContext); + } else { + uri = HttpRequestInfo.GetPublicFacingUrl(_requestContext.Request, _requestContext.Request.ServerVariables); + } + // 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, _authenticationProvider.ProviderName); + _authenticationProvider.RequestAuthentication(_requestContext, uri); + } + + public static string GetProviderName(HttpContextBase context) { + return context.Request.QueryString[ProviderQueryStringName]; + } + + /// <summary> + /// Checks if user is successfully authenticated when user is redirected back to this user. + /// </summary> + /// <returns></returns> + public AuthenticationResult VerifyAuthentication() { + AuthenticationResult result = _authenticationProvider.VerifyAuthentication(_requestContext); + 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: _authenticationProvider.ProviderName, + providerUserId: null, + userName: null, + extraData: null); + } + + return result; + } + + /// <summary> + /// Checks if the specified provider user id represents a valid account. + /// If it does, log user in. + /// </summary> + /// <param name="providerUserId">The provider user id.</param> + /// <param name="createPersistentCookie">if set to <c>true</c> create persistent cookie.</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 bool Login(string providerUserId, bool createPersistentCookie) { + string userName = _dataProvider.GetUserNameFromOpenAuth(_authenticationProvider.ProviderName, providerUserId); + if (String.IsNullOrEmpty(userName)) { + return false; + } + + OpenAuthAuthenticationTicketHelper.SetAuthenticationTicket(_requestContext, userName, createPersistentCookie); + return true; + } + + /// <summary> + /// Gets a value indicating whether the current user is authenticated by an OAuth & OpenID provider. + /// </summary> + public bool IsAuthenticatedWithOpenAuth { + get { + return _requestContext.Request.IsAuthenticated && + OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(_requestContext); + } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.AspNet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..30776c7 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +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.AspNet")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("DotNetOpenAuth.AspNet")] +[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")] + +#if StrongNameSigned +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +#else +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test")] +#endif
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/Resources/WebResources.Designer.cs b/src/DotNetOpenAuth.AspNet/Resources/WebResources.Designer.cs new file mode 100644 index 0000000..624aa2b --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Resources/WebResources.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// <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.AspNet.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.AspNet.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 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); + } + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/Resources/WebResources.resx b/src/DotNetOpenAuth.AspNet/Resources/WebResources.resx new file mode 100644 index 0000000..dce188c --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Resources/WebResources.resx @@ -0,0 +1,138 @@ +<?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="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> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/UriHelper.cs b/src/DotNetOpenAuth.AspNet/UriHelper.cs new file mode 100644 index 0000000..83e0ade --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/UriHelper.cs @@ -0,0 +1,66 @@ +namespace DotNetOpenAuth.AspNet { + using System; + using System.Text.RegularExpressions; + using System.Web; + using DotNetOpenAuth.Messaging; + + 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> + /// 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, HttpContextBase context) { + if (Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute)) { + return new Uri(returnUrl, UriKind.Absolute); + } + + if (!VirtualPathUtility.IsAbsolute(returnUrl)) { + returnUrl = VirtualPathUtility.ToAbsolute(returnUrl); + } + + Uri publicUrl = HttpRequestInfo.GetPublicFacingUrl(context.Request, context.Request.ServerVariables); + return new Uri(publicUrl, returnUrl); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln index ddf80bd..3ccdfae 100644 --- a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln +++ b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln @@ -29,6 +29,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.BuildTasks", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuGet", "NuGet", "{D49E2011-0E1C-4AB5-9887-BD1D42266503}" ProjectSection(SolutionItems) = preProject + ..\..\nuget\DotNetOpenAuth.AspNet.nuspec = ..\..\nuget\DotNetOpenAuth.AspNet.nuspec ..\..\nuget\DotNetOpenAuth.Core.nuspec = ..\..\nuget\DotNetOpenAuth.Core.nuspec ..\..\nuget\DotNetOpenAuth.OAuth.Consumer.nuspec = ..\..\nuget\DotNetOpenAuth.OAuth.Consumer.nuspec ..\..\nuget\DotNetOpenAuth.OAuth.Core.nuspec = ..\..\nuget\DotNetOpenAuth.OAuth.Core.nuspec diff --git a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj index 9b8a613..f669731 100644 --- a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj +++ b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj @@ -127,6 +127,7 @@ <Compile Include="Loggers\NoOpLogger.cs" /> <Compile Include="Loggers\TraceLogger.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Messaging\ReadOnlyDictionary.cs" /> <Compile Include="Reporting.cs" /> <Compile Include="Requires.cs" /> <Compile Include="Strings.Designer.cs"> diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs index 49ecb36..579225b 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs @@ -245,7 +245,8 @@ namespace DotNetOpenAuth.Messaging { if (this.InputStream.CanSeek) { this.InputStream.Seek(originalPosition, SeekOrigin.Begin); } - } else { + } + else { this.form = new NameValueCollection(); } } @@ -280,7 +281,8 @@ namespace DotNetOpenAuth.Messaging { if (!this.IsUrlRewritten) { // No rewriting has taken place. this.queryStringBeforeRewriting = this.QueryString; - } else { + } + else { // Rewriting detected! Recover the original request URI. ErrorUtilities.VerifyInternal(this.UrlBeforeRewriting != null, "UrlBeforeRewriting is null, so the query string cannot be determined."); this.queryStringBeforeRewriting = HttpUtility.ParseQueryString(this.UrlBeforeRewriting.Query); @@ -317,6 +319,24 @@ namespace DotNetOpenAuth.Messaging { /// is a read-only kind of <see cref="NameValueCollection"/>. /// </remarks> internal static Uri GetPublicFacingUrl(HttpRequest request, NameValueCollection serverVariables) { + return GetPublicFacingUrl(new HttpRequestWrapper(request), serverVariables); + } + + /// <summary> + /// Gets the public facing URL for the given incoming HTTP request. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="serverVariables">The server variables to consider part of the request.</param> + /// <returns> + /// The URI that the outside world used to create this request. + /// </returns> + /// <remarks> + /// Although the <paramref name="serverVariables"/> value can be obtained from + /// <see cref="HttpRequest.ServerVariables"/>, it's useful to be able to pass them + /// in so we can simulate injected values from our unit tests since the actual property + /// is a read-only kind of <see cref="NameValueCollection"/>. + /// </remarks> + internal static Uri GetPublicFacingUrl(HttpRequestBase request, NameValueCollection serverVariables) { Requires.NotNull(request, "request"); Requires.NotNull(serverVariables, "serverVariables"); @@ -336,7 +356,8 @@ namespace DotNetOpenAuth.Messaging { publicRequestUri.Host = hostAndPort.Host; publicRequestUri.Port = hostAndPort.Port; // CC missing Uri.Port contract that's on UriBuilder.Port return publicRequestUri.Uri; - } else { + } + else { // Failover to the method that works for non-web farm enviroments. // We use Request.Url for the full path to the server, and modify it // with Request.RawUrl to capture both the cookieless session "directory" if it exists @@ -358,7 +379,8 @@ namespace DotNetOpenAuth.Messaging { NameValueCollection query; if (this.HttpMethod == "GET") { query = this.QueryStringBeforeRewriting; - } else { + } + else { query = this.Form; } return query; @@ -396,7 +418,8 @@ namespace DotNetOpenAuth.Messaging { foreach (string key in pairs) { try { headers.Add(key, pairs[key]); - } catch (ArgumentException ex) { + } + catch (ArgumentException ex) { Logger.Messaging.WarnFormat( "{0} thrown when trying to add web header \"{1}: {2}\". {3}", ex.GetType().Name, diff --git a/src/DotNetOpenAuth.Core/Messaging/ReadOnlyDictionary.cs b/src/DotNetOpenAuth.Core/Messaging/ReadOnlyDictionary.cs new file mode 100644 index 0000000..0ba1ff5 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/ReadOnlyDictionary.cs @@ -0,0 +1,224 @@ +//----------------------------------------------------------------------- +// <copyright file="ReadOnlyDictionary.cs" company="Microsoft Corporation"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + + /// <summary> + /// Represents a read-only dictionary. + /// </summary> + /// <typeparam name="K">The type of the key.</typeparam> + /// <typeparam name="V">The type of the value.</typeparam> + internal class ReadOnlyDictionary<K, V> : IDictionary<K, V> { + /// <summary> + /// Contains base dictionary. + /// </summary> + private readonly IDictionary<K, V> baseDictionary; + + /// <summary> + /// Initializes a new instance of the <see cref="ReadOnlyDictionary<K, V>"/> class. + /// </summary> + /// <param name="baseDictionary">The base dictionary.</param> + public ReadOnlyDictionary(IDictionary<K, V> baseDictionary) { + this.baseDictionary = baseDictionary; + } + + /// <summary> + /// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </summary> + /// <returns> + /// An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the keys of the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </returns> + public ICollection<K> Keys { + get { return this.baseDictionary.Keys; } + } + + /// <summary> + /// Gets an <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </summary> + /// <returns> + /// An <see cref="T:System.Collections.Generic.ICollection`1"/> containing the values in the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </returns> + public ICollection<V> Values { + get { return this.baseDictionary.Values; } + } + + /// <summary> + /// Gets the number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>. + /// </summary> + /// <returns> + /// The number of elements contained in the <see cref="T:System.Collections.Generic.ICollection`1"/>. + /// </returns> + public int Count { + get { return this.baseDictionary.Count; } + } + + /// <summary> + /// Gets a value indicating whether the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. + /// </summary> + /// <returns>true if the <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only; otherwise, false. + /// </returns> + public bool IsReadOnly { + get { return true; } + } + + /// <summary> + /// Gets or sets the element with the specified key. + /// </summary> + /// <param name="key">The key being read or written.</param> + /// <returns> + /// The element with the specified key. + /// </returns> + /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null. + /// </exception> + /// <exception cref="T:System.Collections.Generic.KeyNotFoundException"> + /// The property is retrieved and <paramref name="key"/> is not found. + /// </exception> + /// <exception cref="T:System.NotSupportedException"> + /// The property is set and the <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only. + /// </exception> + public V this[K key] { + get { return this.baseDictionary[key]; } + set { throw new NotSupportedException(); } + } + + /// <summary> + /// Adds an element with the provided key and value to the <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </summary> + /// <param name="key">The object to use as the key of the element to add.</param> + /// <param name="value">The object to use as the value of the element to add.</param> + /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null. + /// </exception> + /// <exception cref="T:System.ArgumentException"> + /// An element with the same key already exists in the <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </exception> + /// <exception cref="T:System.NotSupportedException"> + /// The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only. + /// </exception> + public void Add(K key, V value) { + throw new NotSupportedException(); + } + + /// <summary> + /// Determines whether the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key. + /// </summary> + /// <param name="key">The key to locate in the <see cref="T:System.Collections.Generic.IDictionary`2"/>.</param> + /// <returns> + /// true if the <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the key; otherwise, false. + /// </returns> + /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null. + /// </exception> + public bool ContainsKey(K key) { + return this.baseDictionary.ContainsKey(key); + } + + /// <summary> + /// Removes the element with the specified key from the <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </summary> + /// <param name="key">The key of the element to remove.</param> + /// <returns> + /// true if the element is successfully removed; otherwise, false. This method also returns false if <paramref name="key"/> was not found in the original <see cref="T:System.Collections.Generic.IDictionary`2"/>. + /// </returns> + /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null. + /// </exception> + /// <exception cref="T:System.NotSupportedException"> + /// The <see cref="T:System.Collections.Generic.IDictionary`2"/> is read-only. + /// </exception> + public bool Remove(K key) { + throw new NotSupportedException(); + } + + /// <summary> + /// Gets the value associated with the specified key. + /// </summary> + /// <param name="key">The key whose value to get.</param> + /// <param name="value">When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the <paramref name="value"/> parameter. This parameter is passed uninitialized.</param> + /// <returns> + /// true if the object that implements <see cref="T:System.Collections.Generic.IDictionary`2"/> contains an element with the specified key; otherwise, false. + /// </returns> + /// <exception cref="T:System.ArgumentNullException"><paramref name="key"/> is null. + /// </exception> + public bool TryGetValue(K key, out V value) { + return this.baseDictionary.TryGetValue(key, out value); + } + + /// <summary> + /// Adds an item to the <see cref="T:System.Collections.Generic.ICollection`1"/>. + /// </summary> + /// <param name="item">The object to add to the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param> + /// <exception cref="T:System.NotSupportedException"> + /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. + /// </exception> + public void Add(KeyValuePair<K, V> item) { + throw new NotSupportedException(); + } + + /// <summary> + /// Removes all items from the <see cref="T:System.Collections.Generic.ICollection`1"/>. + /// </summary> + /// <exception cref="T:System.NotSupportedException"> + /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. + /// </exception> + public void Clear() { + throw new NotSupportedException(); + } + + /// <summary> + /// Determines whether the <see cref="T:System.Collections.Generic.ICollection`1"/> contains a specific value. + /// </summary> + /// <param name="item">The object to locate in the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param> + /// <returns> + /// true if <paramref name="item"/> is found in the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. + /// </returns> + public bool Contains(KeyValuePair<K, V> item) { + return this.baseDictionary.Contains(item); + } + + /// <summary> + /// Copies to. + /// </summary> + /// <param name="array">The array.</param> + /// <param name="arrayIndex">Index of the array.</param> + public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex) { + this.baseDictionary.CopyTo(array, arrayIndex); + } + + /// <summary> + /// Removes the first occurrence of a specific object from the <see cref="T:System.Collections.Generic.ICollection`1"/>. + /// </summary> + /// <param name="item">The object to remove from the <see cref="T:System.Collections.Generic.ICollection`1"/>.</param> + /// <returns> + /// true if <paramref name="item"/> was successfully removed from the <see cref="T:System.Collections.Generic.ICollection`1"/>; otherwise, false. This method also returns false if <paramref name="item"/> is not found in the original <see cref="T:System.Collections.Generic.ICollection`1"/>. + /// </returns> + /// <exception cref="T:System.NotSupportedException"> + /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. + /// </exception> + public bool Remove(KeyValuePair<K, V> item) { + throw new NotSupportedException(); + } + + /// <summary> + /// Returns an enumerator that iterates through the collection. + /// </summary> + /// <returns> + /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection. + /// </returns> + public IEnumerator<KeyValuePair<K, V>> GetEnumerator() { + return this.baseDictionary.GetEnumerator(); + } + + /// <summary> + /// Returns an enumerator that iterates through a collection. + /// </summary> + /// <returns> + /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection. + /// </returns> + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { + return this.baseDictionary.GetEnumerator(); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs index 950a7cd..e57b211 100644 --- a/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs +++ b/src/DotNetOpenAuth.Core/Properties/AssemblyInfo.cs @@ -67,6 +67,8 @@ 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.AspNet.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] #else [assembly: InternalsVisibleTo("DotNetOpenAuth.Test")] @@ -88,5 +90,7 @@ using System.Web.UI; [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.AuthorizationServer")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.ResourceServer")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2.Client")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] #endif diff --git a/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs index cbc1307..7f63d1b 100644 --- a/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs +++ b/src/DotNetOpenAuth.OAuth/Properties/AssemblyInfo.cs @@ -53,6 +53,7 @@ using System.Web.UI; [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.ServiceProvider, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OpenIdOAuth, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] #else [assembly: InternalsVisibleTo("DotNetOpenAuth.Test")] @@ -60,5 +61,6 @@ using System.Web.UI; [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth.ServiceProvider")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OpenIdOAuth")] [assembly: InternalsVisibleTo("DotNetOpenAuth.OAuth2")] +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] #endif diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index 2aff83c..73e27f8 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -211,6 +211,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OAuthServiceProvider", "..\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.OpenIdOAuth", "DotNetOpenAuth.OpenIdOAuth\DotNetOpenAuth.OpenIdOAuth.csproj", "{4BFAA336-5DF3-4F27-82D3-06D13240E8AB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.AspNet", "DotNetOpenAuth.AspNet\DotNetOpenAuth.AspNet.csproj", "{51835086-9611-4C53-819B-F2D5C9320873}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.AspNet.Test", "DotNetOpenAuth.AspNet.Test\DotNetOpenAuth.AspNet.Test.csproj", "{C23B217B-4D35-4A72-A1F7-FAEB4F39CB91}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.OAuth.Common", "DotNetOpenAuth.OAuth.Common\DotNetOpenAuth.OAuth.Common.csproj", "{115217C5-22CD-415C-A292-0DD0238CDD89}" EndProject Global @@ -609,6 +613,7 @@ Global {60426312-6AE5-4835-8667-37EDEA670222} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} {57A7DD35-666C-4FA3-9A1B-38961E50CA27} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} {115217C5-22CD-415C-A292-0DD0238CDD89} = {8D4236F7-C49B-49D3-BA71-6B86C9514BDE} + {51835086-9611-4C53-819B-F2D5C9320873} = {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} |