diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2010-05-06 21:18:11 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2010-05-06 21:18:11 -0700 |
commit | 3376d7a58ffdae11afa3b8ac4a946d491808576a (patch) | |
tree | 0093148a9b3cd3f93f8666ba394cbf642383b5ff /src | |
parent | 91f6fca9adfd016f913cab905a986d68117f6caa (diff) | |
parent | 998cfe308c340b70e1497bff3e25c47a194c7f7c (diff) | |
download | DotNetOpenAuth-3376d7a58ffdae11afa3b8ac4a946d491808576a.zip DotNetOpenAuth-3376d7a58ffdae11afa3b8ac4a946d491808576a.tar.gz DotNetOpenAuth-3376d7a58ffdae11afa3b8ac4a946d491808576a.tar.bz2 |
Merge branch 'v3.4' into oauthWRAP
Conflicts:
samples/OAuthConsumer/Web.config
src/DotNetOpenAuth/DotNetOpenAuth.csproj
src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs
src/DotNetOpenAuth/Messaging/MessagingStrings.resx
src/version.txt
Diffstat (limited to 'src')
76 files changed, 2496 insertions, 887 deletions
diff --git a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj index 3b074a6..ee39ba3 100644 --- a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj +++ b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj @@ -377,6 +377,12 @@ <Install>true</Install> </BootstrapperPackage> </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="OpenId\Discovery\htmldiscovery\html20provWithEmptyXrds.html" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="OpenId\Discovery\htmldiscovery\html20provWithBadXrds.html" /> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> </Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Test/Logging.config b/src/DotNetOpenAuth.Test/Logging.config index cd19de2..87da027 100644 --- a/src/DotNetOpenAuth.Test/Logging.config +++ b/src/DotNetOpenAuth.Test/Logging.config @@ -30,7 +30,4 @@ <logger name="DotNetOpenAuth.Test"> <level value="Debug" /> </logger> - <logger name="DotNetOpenAuth.OpenId.ChannelElements.SigningBindingElement"> - <level value="WARN" /> - </logger> </log4net> diff --git a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs index 8d5ef2a..6160680 100644 --- a/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/ChannelElements/SigningBindingElementTests.cs @@ -21,7 +21,7 @@ namespace DotNetOpenAuth.Test.OpenId.ChannelElements { /// </summary> [TestCase] public void SignaturesMatchKnownGood() { - Protocol protocol = Protocol.Default; + Protocol protocol = Protocol.V20; var settings = new ProviderSecuritySettings(); var store = new AssociationMemoryStore<AssociationRelyingPartyType>(); byte[] associationSecret = Convert.FromBase64String("rsSwv1zPWfjPRQU80hciu8FPDC+GONAMJQ/AvSo1a2M="); diff --git a/src/DotNetOpenAuth.Test/OpenId/Discovery/htmldiscovery/html20provWithBadXrds.html b/src/DotNetOpenAuth.Test/OpenId/Discovery/htmldiscovery/html20provWithBadXrds.html new file mode 100644 index 0000000..e695116 --- /dev/null +++ b/src/DotNetOpenAuth.Test/OpenId/Discovery/htmldiscovery/html20provWithBadXrds.html @@ -0,0 +1,10 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Untitled Page</title> + <meta http-equiv="X-XRDS-Location" content="http://localhost/xrds-notfound.xml"/> + <link rel="openid2.provider" href="http://a/b" /> +</head> +<body> +</body> +</html> diff --git a/src/DotNetOpenAuth.Test/OpenId/Discovery/htmldiscovery/html20provWithEmptyXrds.html b/src/DotNetOpenAuth.Test/OpenId/Discovery/htmldiscovery/html20provWithEmptyXrds.html new file mode 100644 index 0000000..97ad7dc --- /dev/null +++ b/src/DotNetOpenAuth.Test/OpenId/Discovery/htmldiscovery/html20provWithEmptyXrds.html @@ -0,0 +1,10 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Untitled Page</title> + <meta http-equiv="X-XRDS-Location" content="http://localhost/xrds-irrelevant.xml"/> + <link rel="openid2.provider" href="http://a/b" /> +</head> +<body> +</body> +</html> diff --git a/src/DotNetOpenAuth.Test/OpenId/DiscoveryServices/UriDiscoveryServiceTests.cs b/src/DotNetOpenAuth.Test/OpenId/DiscoveryServices/UriDiscoveryServiceTests.cs index 7b12a1d..1050b4b 100644 --- a/src/DotNetOpenAuth.Test/OpenId/DiscoveryServices/UriDiscoveryServiceTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/DiscoveryServices/UriDiscoveryServiceTests.cs @@ -179,6 +179,23 @@ namespace DotNetOpenAuth.Test.OpenId.DiscoveryServices { } /// <summary> + /// Verifies HTML discovery proceeds if an XRDS document is referenced that doesn't contain OpenID endpoints. + /// </summary> + [TestCase] + public void HtmlDiscoveryProceedsIfXrdsIsEmpty() { + this.MockResponder.RegisterMockResponse(new Uri("http://localhost/xrds-irrelevant.xml"), "application/xrds+xml", LoadEmbeddedFile("/Discovery/xrdsdiscovery/xrds-irrelevant.xml")); + this.DiscoverHtml("html20provWithEmptyXrds", ProtocolVersion.V20, null, "http://a/b"); + } + + /// <summary> + /// Verifies HTML discovery proceeds if the XRDS that is referenced cannot be found. + /// </summary> + [TestCase] + public void HtmlDiscoveryProceedsIfXrdsIsBadOrMissing() { + this.DiscoverHtml("html20provWithBadXrds", ProtocolVersion.V20, null, "http://a/b"); + } + + /// <summary> /// Verifies that a dual identifier yields only one service endpoint by default. /// </summary> [TestCase] diff --git a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperOPTests.cs b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperOPTests.cs index ffbadc8..1b1dd49 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperOPTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperOPTests.cs @@ -42,6 +42,8 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions { Assert.IsNull(sreg); // Make sure we're still able to send an sreg response. + // (not really a valid scenario, since OPs don't have public access + // to directly create a response without a request. var sregResponse = new ClaimsResponse(); this.request.AddResponseExtension(sregResponse); ExtensionsInteropHelper.ConvertSregToMatchRequest(this.request); @@ -49,12 +51,18 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions { Assert.AreSame(sregResponse, extensions.Single()); } + [TestCase] + public void NegativeResponse() { + this.request.IsAuthenticated = false; + ExtensionsInteropHelper.ConvertSregToMatchRequest(this.request); + } + /// <summary> /// Verifies sreg coming in is seen as sreg. /// </summary> [TestCase] public void UnifyExtensionsAsSregWithSreg() { - var sregInjected = new ClaimsRequest { + var sregInjected = new ClaimsRequest(DotNetOpenAuth.OpenId.Extensions.SimpleRegistration.Constants.sreg_ns) { Nickname = DemandLevel.Request, }; this.extensions.Add(sregInjected); @@ -63,7 +71,7 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions { Assert.AreEqual(DemandLevel.Request, sreg.Nickname); Assert.AreEqual(DemandLevel.NoRequest, sreg.FullName); - var sregResponse = new ClaimsResponse(); + var sregResponse = sreg.CreateResponse(); this.request.AddResponseExtension(sregResponse); ExtensionsInteropHelper.ConvertSregToMatchRequest(this.request); var extensions = this.GetResponseExtensions(); @@ -91,7 +99,7 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions { /// </summary> [TestCase] public void UnifyExtensionsAsSregWithBothSregAndAX() { - var sregInjected = new ClaimsRequest { + var sregInjected = new ClaimsRequest(DotNetOpenAuth.OpenId.Extensions.SimpleRegistration.Constants.sreg_ns) { Nickname = DemandLevel.Request, }; this.extensions.Add(sregInjected); @@ -103,9 +111,8 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions { Assert.AreEqual(DemandLevel.Request, sreg.Nickname); Assert.AreEqual(DemandLevel.NoRequest, sreg.Email); - var sregResponseInjected = new ClaimsResponse { - Nickname = "andy", - }; + var sregResponseInjected = sreg.CreateResponse(); + sregResponseInjected.Nickname = "andy"; this.request.AddResponseExtension(sregResponseInjected); var axResponseInjected = new FetchResponse(); axResponseInjected.Attributes.Add(WellKnownAttributes.Contact.Email, "a@b.com"); @@ -134,9 +141,8 @@ namespace DotNetOpenAuth.Test.OpenId.Extensions { Assert.AreEqual(DemandLevel.Require, sreg.FullName); Assert.AreEqual(DemandLevel.NoRequest, sreg.Language); - var sregResponse = new ClaimsResponse { - Nickname = "andy", - }; + var sregResponse = sreg.CreateResponse(); + sregResponse.Nickname = "andy"; this.request.AddResponseExtension(sregResponse); ExtensionsInteropHelper.ConvertSregToMatchRequest(this.request); var extensions = this.GetResponseExtensions(); diff --git a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPRequestTests.cs b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPRequestTests.cs index 181c023..44a629a 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPRequestTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPRequestTests.cs @@ -4,7 +4,7 @@ // </copyright> //----------------------------------------------------------------------- -namespace DotNetOpenAuth.Test.OpenId { +namespace DotNetOpenAuth.Test.OpenId.Extensions { using System.Collections.ObjectModel; using System.Linq; using DotNetOpenAuth.OpenId; diff --git a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPResponseTests.cs b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPResponseTests.cs index 9074f75..7b528d0 100644 --- a/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPResponseTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/Extensions/ExtensionsInteropHelperRPResponseTests.cs @@ -4,7 +4,7 @@ // </copyright> //----------------------------------------------------------------------- -namespace DotNetOpenAuth.Test.OpenId { +namespace DotNetOpenAuth.Test.OpenId.Extensions { using System.Collections.Generic; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId; diff --git a/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs b/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs index 811b7d1..25b0607 100644 --- a/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/RelyingParty/PositiveAuthenticationResponseTests.cs @@ -12,6 +12,7 @@ namespace DotNetOpenAuth.Test.OpenId.RelyingParty { using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration; using DotNetOpenAuth.OpenId.Messages; using DotNetOpenAuth.OpenId.RelyingParty; + using DotNetOpenAuth.Test.Mocks; using NUnit.Framework; [TestFixture] @@ -120,6 +121,26 @@ namespace DotNetOpenAuth.Test.OpenId.RelyingParty { Assert.IsNull(authResponse.GetCallbackArgument("a")); } + /// <summary> + /// Verifies that certain problematic claimed identifiers pass through to the RP response correctly. + /// </summary> + [TestCase] + public void ProblematicClaimedId() { + var providerEndpoint = new ProviderEndpointDescription(OpenIdTestBase.OPUri, Protocol.Default.Version); + string claimed_id = BaseMockUri + "a./b."; + var se = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimed_id, claimed_id, providerEndpoint, null, null); + UriIdentifier identityUri = (UriIdentifier)se.ClaimedIdentifier; + var mockId = new MockIdentifier(identityUri, this.MockResponder, new IdentifierDiscoveryResult[] { se }); + + var positiveAssertion = this.GetPositiveAssertion(); + positiveAssertion.ClaimedIdentifier = mockId; + positiveAssertion.LocalIdentifier = mockId; + var rp = CreateRelyingParty(); + var authResponse = new PositiveAuthenticationResponse(positiveAssertion, rp); + Assert.AreEqual(AuthenticationStatus.Authenticated, authResponse.Status); + Assert.AreEqual(claimed_id, authResponse.ClaimedIdentifier.ToString()); + } + private PositiveAssertionResponse GetPositiveAssertion() { return this.GetPositiveAssertion(false); } diff --git a/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs b/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs index 73c185e..5b015ff 100644 --- a/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/UriIdentifierTests.cs @@ -112,22 +112,51 @@ namespace DotNetOpenAuth.Test.OpenId { Identifier noFragment = UriIdentifier.Parse("http://a/b"); Identifier fragment = UriIdentifier.Parse("http://a/b#c"); Assert.AreSame(noFragment, noFragment.TrimFragment()); - Assert.AreEqual(noFragment, fragment.TrimFragment()); + Assert.AreEqual(noFragment.ToString(), fragment.TrimFragment().ToString()); + + // Try the problematic ones + TestAsFullAndPartialTrust(fullTrust => { + Identifier noFrag = UriIdentifier.Parse("http://a/b./c"); + Identifier frag = UriIdentifier.Parse("http://a/b./c#d"); + Assert.AreSame(noFrag, noFrag.TrimFragment()); + Assert.AreEqual(noFrag.ToString(), frag.TrimFragment().ToString()); + }); } [TestCase] public void ToStringTest() { Assert.AreEqual(this.goodUri, new UriIdentifier(this.goodUri).ToString()); + TestAsFullAndPartialTrust(fullTrust => { + Assert.AreEqual("http://abc/D./e.?Qq#Ff", new UriIdentifier("HTTP://ABC/D./e.?Qq#Ff").ToString()); + Assert.AreEqual("http://abc/D./e.?Qq", new UriIdentifier("HTTP://ABC/D./e.?Qq").ToString()); + Assert.AreEqual("http://abc/D./e.#Ff", new UriIdentifier("HTTP://ABC/D./e.#Ff").ToString()); + Assert.AreEqual("http://abc/", new UriIdentifier("HTTP://ABC").ToString()); + Assert.AreEqual("http://abc/?q", new UriIdentifier("HTTP://ABC?q").ToString()); + Assert.AreEqual("http://abc/#f", new UriIdentifier("HTTP://ABC#f").ToString()); + }); } [TestCase] public void EqualsTest() { - Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri)); - // This next test is an interesting side-effect of passing off to Uri.Equals. But it's probably ok. - Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "#frag")); - Assert.AreNotEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "a")); - Assert.AreNotEqual(null, new UriIdentifier(this.goodUri)); - Assert.IsTrue(new UriIdentifier(this.goodUri).Equals(this.goodUri)); + TestAsFullAndPartialTrust(fulltrust => { + Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri)); + // This next test is an interesting side-effect of passing off to Uri.Equals. But it's probably ok. + Assert.AreEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "#frag")); + Assert.AreEqual(new UriIdentifier("http://a/b./c."), new UriIdentifier("http://a/b./c.#frag")); + Assert.AreNotEqual(new UriIdentifier(this.goodUri), new UriIdentifier(this.goodUri + "a")); + Assert.AreNotEqual(null, new UriIdentifier(this.goodUri)); + Assert.IsTrue(new UriIdentifier(this.goodUri).Equals(this.goodUri)); + + Assert.AreEqual(Identifier.Parse("HTTP://WWW.FOO.COM/abc", true), Identifier.Parse("http://www.foo.com/abc", true)); + Assert.AreEqual(Identifier.Parse("HTTP://WWW.FOO.COM/abc", true), Identifier.Parse("http://www.foo.com/abc", false)); + Assert.AreEqual(Identifier.Parse("HTTP://WWW.FOO.COM/abc", false), Identifier.Parse("http://www.foo.com/abc", false)); + Assert.AreNotEqual(Identifier.Parse("http://www.foo.com/abc", true), Identifier.Parse("http://www.foo.com/ABC", true)); + Assert.AreNotEqual(Identifier.Parse("http://www.foo.com/abc", true), Identifier.Parse("http://www.foo.com/ABC", false)); + Assert.AreNotEqual(Identifier.Parse("http://www.foo.com/abc", false), Identifier.Parse("http://www.foo.com/ABC", false)); + + Assert.AreNotEqual(Identifier.Parse("http://a/b./c."), Identifier.Parse("http://a/b/c.")); + Assert.AreEqual(Identifier.Parse("http://a/b./c."), Identifier.Parse("http://a/b./c.")); + }); } [TestCase] @@ -150,6 +179,51 @@ namespace DotNetOpenAuth.Test.OpenId { Assert.AreEqual("https://host:80/PaTH?KeY=VaLUE#fRag", id.ToString()); } + /// <summary> + /// Verifies that URIs that contain base64 encoded path segments (such as Yahoo) are properly preserved. + /// </summary> + /// <remarks> + /// Yahoo includes a base64 encoded part as their last path segment, + /// which may end with a period. The default .NET Uri parser trims off + /// trailing periods, which breaks OpenID unless special precautions are taken. + /// </remarks> + [TestCase] + public void TrailingPeriodsNotTrimmed() { + TestAsFullAndPartialTrust(fullTrust => { + string claimedIdentifier = "https://me.yahoo.com/a/AsDf.#asdf"; + Identifier id = claimedIdentifier; + Assert.AreEqual(claimedIdentifier, id.OriginalString); + Assert.AreEqual(claimedIdentifier, id.ToString()); + + UriIdentifier idUri = new UriIdentifier(claimedIdentifier); + Assert.AreEqual(claimedIdentifier, idUri.OriginalString); + Assert.AreEqual(claimedIdentifier, idUri.ToString()); + if (fullTrust) { + Assert.AreEqual(claimedIdentifier, idUri.Uri.AbsoluteUri); + } + Assert.AreEqual(Uri.UriSchemeHttps, idUri.Uri.Scheme); // in case custom scheme tricks are played, this must still match + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().ToString()); + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().OriginalString); + Assert.AreEqual(id.ToString(), new UriIdentifier((Uri)idUri).ToString(), "Round tripping UriIdentifier->Uri->UriIdentifier failed."); + + idUri = new UriIdentifier(new Uri(claimedIdentifier)); + Assert.AreEqual(claimedIdentifier, idUri.OriginalString); + Assert.AreEqual(claimedIdentifier, idUri.ToString()); + if (fullTrust) { + Assert.AreEqual(claimedIdentifier, idUri.Uri.AbsoluteUri); + } + Assert.AreEqual(Uri.UriSchemeHttps, idUri.Uri.Scheme); // in case custom scheme tricks are played, this must still match + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().ToString()); + Assert.AreEqual("https://me.yahoo.com/a/AsDf.", idUri.TrimFragment().OriginalString); + Assert.AreEqual(id.ToString(), new UriIdentifier((Uri)idUri).ToString(), "Round tripping UriIdentifier->Uri->UriIdentifier failed."); + + claimedIdentifier = "https://me.yahoo.com:443/a/AsDf.#asdf"; + id = claimedIdentifier; + Assert.AreEqual(claimedIdentifier, id.OriginalString); + Assert.AreEqual("https://me.yahoo.com/a/AsDf.#asdf", id.ToString()); + }); + } + [TestCase] public void HttpSchemePrepended() { UriIdentifier id = new UriIdentifier("www.yahoo.com"); @@ -193,5 +267,45 @@ namespace DotNetOpenAuth.Test.OpenId { Assert.AreEqual("http://www.yahoo.com/", secureId.ToString()); Assert.AreEqual(0, Discover(secureId).Count()); } + + /// <summary> + /// Verifies that unicode hostnames are handled. + /// </summary> + [TestCase] + public void UnicodeHostSupport() { + var id = new UriIdentifier("http://server崎/村"); + Assert.AreEqual("server崎", id.Uri.Host); + } + + /// <summary> + /// Verifies SimpleUri behavior + /// </summary> + [TestCase] + public void SimpleUri() { + Assert.AreEqual("http://abc/D./e.?Qq#Ff", new UriIdentifier.SimpleUri("HTTP://ABC/D./e.?Qq#Ff").ToString()); + Assert.AreEqual("http://abc/D./e.?Qq", new UriIdentifier.SimpleUri("HTTP://ABC/D./e.?Qq").ToString()); + Assert.AreEqual("http://abc/D./e.#Ff", new UriIdentifier.SimpleUri("HTTP://ABC/D./e.#Ff").ToString()); + Assert.AreEqual("http://abc/", new UriIdentifier.SimpleUri("HTTP://ABC/").ToString()); + Assert.AreEqual("http://abc/", new UriIdentifier.SimpleUri("HTTP://ABC").ToString()); + Assert.AreEqual("http://abc/?q", new UriIdentifier.SimpleUri("HTTP://ABC?q").ToString()); + Assert.AreEqual("http://abc/#f", new UriIdentifier.SimpleUri("HTTP://ABC#f").ToString()); + + Assert.AreEqual("http://abc/a//b", new UriIdentifier.SimpleUri("http://abc/a//b").ToString()); + Assert.AreEqual("http://abc/a%2Fb/c", new UriIdentifier.SimpleUri("http://abc/a%2fb/c").ToString()); + Assert.AreEqual("http://abc/A/c", new UriIdentifier.SimpleUri("http://abc/%41/c").ToString()); + } + + private static void TestAsFullAndPartialTrust(Action<bool> action) { + // Test a bunch of interesting URLs both with scheme substitution on and off. + Assert.IsTrue(UriIdentifier_Accessor.schemeSubstitution, "Expected scheme substitution to be working."); + action(true); + + UriIdentifier_Accessor.schemeSubstitution = false; + try { + action(false); + } finally { + UriIdentifier_Accessor.schemeSubstitution = true; + } + } } } diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index dcff8d0..60dffe6 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -120,54 +120,6 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "OpenIdRelyingPartyClassicAs EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OAuthConsumerWpf", "..\samples\OAuthConsumerWpf\OAuthConsumerWpf.csproj", "{6EC36418-DBC5-4AD1-A402-413604AA7A08}" - ProjectSection(ProjectDependencies) = postProject - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191} = {7ADCCD5C-AC2B-4340-9410-FE3A31A48191} - EndProjectSection -EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "OAuthConsumer", "..\samples\OAuthConsumer\", "{9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}" - ProjectSection(WebsiteProperties) = preProject - TargetFrameworkMoniker = ".NETFramework,Version%3Dv3.5" - ProjectReferences = "{3191B653-F76D-4C1A-9A5A-347BC3AAAAB7}|DotNetOpenAuth.dll;{AA78D112-D889-414B-A7D4-467B34C7B663}|DotNetOpenAuth.ApplicationBlock.dll;" - Debug.AspNetCompiler.VirtualPath = "/OAuthConsumer" - Debug.AspNetCompiler.PhysicalPath = "..\samples\OAuthConsumer\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\OAuthConsumer\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/OAuthConsumer" - Release.AspNetCompiler.PhysicalPath = "..\samples\OAuthConsumer\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\OAuthConsumer\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - VWDPort = "59721" - StartServerOnDebug = "false" - EndProjectSection -EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "OAuthServiceProvider", "..\samples\OAuthServiceProvider\", "{7ADCCD5C-AC2B-4340-9410-FE3A31A48191}" - ProjectSection(WebsiteProperties) = preProject - TargetFrameworkMoniker = ".NETFramework,Version%3Dv3.5" - ProjectReferences = "{3191B653-F76D-4C1A-9A5A-347BC3AAAAB7}|DotNetOpenAuth.dll;" - Debug.AspNetCompiler.VirtualPath = "/OAuthServiceProvider" - Debug.AspNetCompiler.PhysicalPath = "..\samples\OAuthServiceProvider\" - Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\OAuthServiceProvider\" - Debug.AspNetCompiler.Updateable = "true" - Debug.AspNetCompiler.ForceOverwrite = "true" - Debug.AspNetCompiler.FixedNames = "false" - Debug.AspNetCompiler.Debug = "True" - Release.AspNetCompiler.VirtualPath = "/OAuthServiceProvider" - Release.AspNetCompiler.PhysicalPath = "..\samples\OAuthServiceProvider\" - Release.AspNetCompiler.TargetPath = "PrecompiledWeb\OAuthServiceProvider\" - Release.AspNetCompiler.Updateable = "true" - Release.AspNetCompiler.ForceOverwrite = "true" - Release.AspNetCompiler.FixedNames = "false" - Release.AspNetCompiler.Debug = "False" - VWDPort = "65169" - VWDDynamicPort = "false" - StartServerOnDebug = "false" - EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIdOfflineProvider", "..\samples\OpenIdOfflineProvider\OpenIdOfflineProvider.csproj", "{5C65603B-235F-47E6-B536-06385C60DE7F}" EndProject @@ -188,6 +140,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIdWebRingSsoProvider", EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "OpenIdRelyingPartyWebFormsVB", "..\samples\OpenIdRelyingPartyWebFormsVB\OpenIdRelyingPartyWebFormsVB.vbproj", "{F289B925-4307-4BEC-B411-885CE70E3379}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OAuthConsumer", "..\samples\OAuthConsumer\OAuthConsumer.csproj", "{9529606E-AF76-4387-BFB7-3D10A5B399AA}" + ProjectSection(ProjectDependencies) = postProject + {E135F455-0669-49F8-9207-07FCA8C8FC79} = {E135F455-0669-49F8-9207-07FCA8C8FC79} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OAuthServiceProvider", "..\samples\OAuthServiceProvider\OAuthServiceProvider.csproj", "{E135F455-0669-49F8-9207-07FCA8C8FC79}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeAnalysis|Any CPU = CodeAnalysis|Any CPU @@ -261,18 +220,6 @@ Global {6EC36418-DBC5-4AD1-A402-413604AA7A08}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EC36418-DBC5-4AD1-A402-413604AA7A08}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EC36418-DBC5-4AD1-A402-413604AA7A08}.Release|Any CPU.Build.0 = Release|Any CPU - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3}.Release|Any CPU.Build.0 = Debug|Any CPU - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191}.Release|Any CPU.Build.0 = Debug|Any CPU {5C65603B-235F-47E6-B536-06385C60DE7F}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU {5C65603B-235F-47E6-B536-06385C60DE7F}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU {5C65603B-235F-47E6-B536-06385C60DE7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -324,6 +271,18 @@ Global {F289B925-4307-4BEC-B411-885CE70E3379}.Debug|Any CPU.Build.0 = Debug|Any CPU {F289B925-4307-4BEC-B411-885CE70E3379}.Release|Any CPU.ActiveCfg = Release|Any CPU {F289B925-4307-4BEC-B411-885CE70E3379}.Release|Any CPU.Build.0 = Release|Any CPU + {9529606E-AF76-4387-BFB7-3D10A5B399AA}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {9529606E-AF76-4387-BFB7-3D10A5B399AA}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {9529606E-AF76-4387-BFB7-3D10A5B399AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9529606E-AF76-4387-BFB7-3D10A5B399AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9529606E-AF76-4387-BFB7-3D10A5B399AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9529606E-AF76-4387-BFB7-3D10A5B399AA}.Release|Any CPU.Build.0 = Release|Any CPU + {E135F455-0669-49F8-9207-07FCA8C8FC79}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {E135F455-0669-49F8-9207-07FCA8C8FC79}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {E135F455-0669-49F8-9207-07FCA8C8FC79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E135F455-0669-49F8-9207-07FCA8C8FC79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E135F455-0669-49F8-9207-07FCA8C8FC79}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E135F455-0669-49F8-9207-07FCA8C8FC79}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -343,8 +302,8 @@ Global {0B4EB2A8-283D-48FB-BCD0-85B8DFFE05E4} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {F289B925-4307-4BEC-B411-885CE70E3379} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {6EC36418-DBC5-4AD1-A402-413604AA7A08} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} - {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} - {7ADCCD5C-AC2B-4340-9410-FE3A31A48191} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} + {9529606E-AF76-4387-BFB7-3D10A5B399AA} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} + {E135F455-0669-49F8-9207-07FCA8C8FC79} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} {6EB90284-BD15-461C-BBF2-131CF55F7C8B} = {8A5CEDB9-7F8A-4BE2-A1B9-97130F453277} {5C65603B-235F-47E6-B536-06385C60DE7F} = {E9ED920D-1F83-48C0-9A4B-09CCE505FE6D} {A78F8FC6-7B03-4230-BE41-761E400D6810} = {B9EB8729-4B54-4453-B089-FE6761BA3057} diff --git a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs index 37f9c78..980d90f 100644 --- a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs +++ b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs @@ -144,6 +144,7 @@ using System.Reflection; /// The conversion cannot be performed. /// </exception> public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { + Contract.Assume(destinationType != null, "Missing contract."); if (destinationType.IsInstanceOfType(value)) { return value; } diff --git a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs index 6ba9c4b..61c0fd8 100644 --- a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs +++ b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs @@ -45,7 +45,7 @@ namespace DotNetOpenAuth.ComponentModel { return null; } - MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public); + MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null); return CreateInstanceDescriptor(identifierParse, new object[] { value.ToString() }); } diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd index a9c5965..3164ec5 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuth.xsd @@ -319,7 +319,32 @@ </xs:documentation> </xs:annotation> </xs:attribute> - <xs:attribute name="allowDualPurposeIdentifiers" type="xs:boolean" /> + <xs:attribute name="allowDualPurposeIdentifiers" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether identifiers that are both OP Identifiers and Claimed Identifiers + should ever be recognized as claimed identifiers. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="allowApproximateIdentifierDiscovery" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether certain Claimed Identifiers that exploit + features that .NET does not have the ability to send exact HTTP requests for will + still be allowed by using an approximate HTTP request. + Only impacts hosts running under partial trust. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="protectDownlevelReplayAttacks" type="xs:boolean"> + <xs:annotation> + <xs:documentation> + Controls whether the relying party should take special care + to protect users against replay attacks when interoperating with OpenID 1.1 Providers. + </xs:documentation> + </xs:annotation> + </xs:attribute> </xs:complexType> </xs:element> <xs:element name="behaviors"> diff --git a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs index 0d8b768..1bf2ebc 100644 --- a/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs +++ b/src/DotNetOpenAuth/Configuration/OpenIdRelyingPartySecuritySettingsElement.cs @@ -71,6 +71,16 @@ namespace DotNetOpenAuth.Configuration { private const string AllowDualPurposeIdentifiersConfigName = "allowDualPurposeIdentifiers"; /// <summary> + /// Gets the name of the @allowApproximateIdentifierDiscovery attribute. + /// </summary> + private const string AllowApproximateIdentifierDiscoveryConfigName = "allowApproximateIdentifierDiscovery"; + + /// <summary> + /// Gets the name of the @protectDownlevelReplayAttacks attribute. + /// </summary> + private const string ProtectDownlevelReplayAttacksConfigName = "protectDownlevelReplayAttacks"; + + /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingPartySecuritySettingsElement"/> class. /// </summary> public OpenIdRelyingPartySecuritySettingsElement() { @@ -201,6 +211,30 @@ namespace DotNetOpenAuth.Configuration { } /// <summary> + /// Gets or sets a value indicating whether certain Claimed Identifiers that exploit + /// features that .NET does not have the ability to send exact HTTP requests for will + /// still be allowed by using an approximate HTTP request. + /// </summary> + /// <value> + /// The default value is <c>true</c>. + /// </value> + [ConfigurationProperty(AllowApproximateIdentifierDiscoveryConfigName, DefaultValue = true)] + public bool AllowApproximateIdentifierDiscovery { + get { return (bool)this[AllowApproximateIdentifierDiscoveryConfigName]; } + set { this[AllowApproximateIdentifierDiscoveryConfigName] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether the Relying Party should take special care + /// to protect users against replay attacks when interoperating with OpenID 1.1 Providers. + /// </summary> + [ConfigurationProperty(ProtectDownlevelReplayAttacksConfigName, DefaultValue = RelyingPartySecuritySettings.ProtectDownlevelReplayAttacksDefault)] + public bool ProtectDownlevelReplayAttacks { + get { return (bool)this[ProtectDownlevelReplayAttacksConfigName]; } + set { this[ProtectDownlevelReplayAttacksConfigName] = value; } + } + + /// <summary> /// Initializes a programmatically manipulatable bag of these security settings with the settings from the config file. /// </summary> /// <returns>The newly created security settings object.</returns> @@ -219,6 +253,8 @@ namespace DotNetOpenAuth.Configuration { settings.RejectDelegatingIdentifiers = this.RejectDelegatingIdentifiers; settings.IgnoreUnsignedExtensions = this.IgnoreUnsignedExtensions; settings.AllowDualPurposeIdentifiers = this.AllowDualPurposeIdentifiers; + settings.AllowApproximateIdentifierDiscovery = this.AllowApproximateIdentifierDiscovery; + settings.ProtectDownlevelReplayAttacks = this.ProtectDownlevelReplayAttacks; return settings; } diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 292da82..3edb13d 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -163,7 +163,7 @@ http://opensource.org/licenses/ms-pl.html <CodeContractsEmitXMLDocs>True</CodeContractsEmitXMLDocs> <CodeContractsRedundantAssumptions>False</CodeContractsRedundantAssumptions> <CodeContractsReferenceAssembly>Build</CodeContractsReferenceAssembly> - <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> + <CodeAnalysisRuleSet>Migrated rules for DotNetOpenAuth.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> <Reference Include="log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821, processorArchitecture=MSIL"> @@ -303,6 +303,8 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> <Compile Include="Messaging\Reflection\IMessagePartNullEncoder.cs" /> <Compile Include="Messaging\Reflection\MessageDescriptionCollection.cs" /> + <Compile Include="Mvc\OpenIdHelper.cs" /> + <Compile Include="Mvc\OpenIdAjaxOptions.cs" /> <Compile Include="Messaging\StandardMessageFactory.cs" /> <Compile Include="OAuthWrap\AuthorizationState.cs" /> <Compile Include="OAuthWrap\IClientTokenManager.cs" /> @@ -552,7 +554,9 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OpenId\RelyingParty\AssociationPreference.cs" /> <Compile Include="OpenId\RelyingParty\AuthenticationRequest.cs" /> <Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" /> + <Compile Include="OpenId\RelyingParty\DuplicateRequestedHostsComparer.cs" /> <Compile Include="OpenId\RelyingParty\IProviderEndpoint.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdAjaxRelyingParty.cs" /> <Compile Include="OpenId\RelyingParty\SelectorButtonContract.cs" /> <Compile Include="OpenId\RelyingParty\SelectorProviderButton.cs" /> <Compile Include="OpenId\RelyingParty\SelectorOpenIdButton.cs" /> @@ -688,7 +692,7 @@ http://opensource.org/licenses/ms-pl.html </EmbeddedResource> </ItemGroup> <ItemGroup> - <EmbeddedResource Include="OpenId\RelyingParty\openid_login.gif" /> + <EmbeddedResource Include="OpenId\RelyingParty\openid_login.png" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="OpenId\RelyingParty\login_failure.png" /> @@ -775,19 +779,19 @@ http://opensource.org/licenses/ms-pl.html </ItemGroup> <ItemGroup> <SignDependsOn Include="BuildUnifiedProduct" /> - <DelaySignedAssemblies Include="$(ILMergeOutputAssembly);
 $(OutputPath)$(ProductName).Contracts.dll;
 " /> - </ItemGroup> - <ItemGroup> + <DelaySignedAssemblies Include="$(ILMergeOutputAssembly); + $(OutputPath)CodeContracts\$(ProductName).Contracts.dll; + " /> <Folder Include="OAuthWrap\Messages\Device\" /> </ItemGroup> - <PropertyGroup> - <!-- Don't sign the non-unified version of the assembly. --> - <SuppressTargetPathDelaySignedAssembly>true</SuppressTargetPathDelaySignedAssembly> - </PropertyGroup> + <PropertyGroup> + <!-- Don't sign the non-unified version of the assembly. --> + <SuppressTargetPathDelaySignedAssembly>true</SuppressTargetPathDelaySignedAssembly> + </PropertyGroup> <Target Name="BuildUnifiedProduct" DependsOnTargets="Build" Inputs="@(ILMergeInputAssemblies)" Outputs="$(ILMergeOutputAssembly)"> <MakeDir Directories="$(ILMergeOutputAssemblyDirectory)" /> <ILMerge ExcludeFile="$(ProjectRoot)ILMergeInternalizeExceptions.txt" InputAssemblies="@(ILMergeInputAssemblies)" OutputFile="$(ILMergeOutputAssembly)" KeyFile="$(PublicKeyFile)" DelaySign="true" /> </Target> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> -</Project>
\ No newline at end of file +</Project> diff --git a/src/DotNetOpenAuth/GlobalSuppressions.cs b/src/DotNetOpenAuth/GlobalSuppressions.cs index e436846..9b1bcfa 100644 --- a/src/DotNetOpenAuth/GlobalSuppressions.cs +++ b/src/DotNetOpenAuth/GlobalSuppressions.cs @@ -57,3 +57,6 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnIncomingRequest(DotNetOpenAuth.OpenId.Provider.IRequest)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.ApplySecuritySettings(DotNetOpenAuth.OpenId.Provider.ProviderSecuritySettings)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Mvc", Scope = "namespace", Target = "DotNetOpenAuth.Mvc")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Portability", "CA1903:UseOnlyApiFromTargetedFramework", MessageId = "System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")] diff --git a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs index 86c1118..ae45229 100644 --- a/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs +++ b/src/DotNetOpenAuth/InfoCard/InfoCardSelector.cs @@ -268,6 +268,7 @@ namespace DotNetOpenAuth.InfoCard { [Category(InfoCardCategory), DefaultValue(PrivacyUrlDefault)] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "We construct a Uri to validate the format of the string.")] [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "That overload is NOT the same.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This can take ~/ paths.")] public string PrivacyUrl { get { return (string)this.ViewState[PrivacyUrlViewStateKey] ?? PrivacyUrlDefault; @@ -570,24 +571,28 @@ namespace DotNetOpenAuth.InfoCard { Panel supportedPanel = new Panel(); - if (!this.DesignMode) { - // At the user agent, assume InfoCard is not supported until - // the JavaScript discovers otherwise and reveals this panel. - supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; - } + try { + if (!this.DesignMode) { + // At the user agent, assume InfoCard is not supported until + // the JavaScript discovers otherwise and reveals this panel. + supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; + } - supportedPanel.Controls.Add(this.CreateInfoCardImage()); + supportedPanel.Controls.Add(this.CreateInfoCardImage()); - // trigger the selector at page load? - if (this.AutoPopup && !this.Page.IsPostBack) { - this.Page.ClientScript.RegisterStartupScript( - typeof(InfoCardSelector), - "selector_load_trigger", - this.GetInfoCardSelectorActivationScript(true), - true); + // trigger the selector at page load? + if (this.AutoPopup && !this.Page.IsPostBack) { + this.Page.ClientScript.RegisterStartupScript( + typeof(InfoCardSelector), + "selector_load_trigger", + this.GetInfoCardSelectorActivationScript(true), + true); + } + return supportedPanel; + } catch { + supportedPanel.Dispose(); + throw; } - - return supportedPanel; } /// <summary> @@ -624,10 +629,15 @@ namespace DotNetOpenAuth.InfoCard { Contract.Ensures(Contract.Result<Panel>() != null); Panel unsupportedPanel = new Panel(); - if (this.UnsupportedTemplate != null) { - this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); + try { + if (this.UnsupportedTemplate != null) { + this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); + } + return unsupportedPanel; + } catch { + unsupportedPanel.Dispose(); + throw; } - return unsupportedPanel; } /// <summary> @@ -692,13 +702,18 @@ namespace DotNetOpenAuth.InfoCard { private Image CreateInfoCardImage() { // add clickable image Image image = new Image(); - image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); - image.AlternateText = InfoCardStrings.SelectorClickPrompt; - image.ToolTip = this.ToolTip; - image.Style[HtmlTextWriterStyle.Cursor] = "hand"; - - image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); - return image; + try { + image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); + image.AlternateText = InfoCardStrings.SelectorClickPrompt; + image.ToolTip = this.ToolTip; + image.Style[HtmlTextWriterStyle.Cursor] = "hand"; + + image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); + return image; + } catch { + image.Dispose(); + throw; + } } /// <summary> diff --git a/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs b/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs index 124f9f8..2ac2b7e 100644 --- a/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs +++ b/src/DotNetOpenAuth/InfoCard/ReceivingTokenEventArgs.cs @@ -74,7 +74,13 @@ namespace DotNetOpenAuth.InfoCard { public void AddDecryptingToken(X509Certificate2 certificate) { Contract.Requires<ArgumentNullException>(certificate != null); Contract.Requires<ArgumentException>(certificate.HasPrivateKey); - this.AddDecryptingToken(new X509SecurityToken(certificate)); + var cert = new X509SecurityToken(certificate); + try { + this.AddDecryptingToken(cert); + } catch { + cert.Dispose(); + throw; + } } #if CONTRACTS_FULL diff --git a/src/DotNetOpenAuth/InfoCard/Token/Token.cs b/src/DotNetOpenAuth/InfoCard/Token/Token.cs index 7fa9a95..89fa3a3 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/Token.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/Token.cs @@ -49,16 +49,18 @@ namespace DotNetOpenAuth.InfoCard { byte[] decryptedBytes; string decryptedString; - using (XmlReader tokenReader = XmlReader.Create(new StringReader(tokenXml))) { - Contract.Assume(tokenReader != null); // BCL contract should say XmlReader.Create result != null - if (IsEncrypted(tokenReader)) { - Logger.InfoCard.DebugFormat("Incoming SAML token, before decryption: {0}", tokenXml); - decryptedBytes = decryptor.DecryptToken(tokenReader); - decryptedString = Encoding.UTF8.GetString(decryptedBytes); - Contract.Assume(decryptedString != null); // BCL contracts should be enhanced here - } else { - decryptedBytes = Encoding.UTF8.GetBytes(tokenXml); - decryptedString = tokenXml; + using (StringReader xmlReader = new StringReader(tokenXml)) { + using (XmlReader tokenReader = XmlReader.Create(xmlReader)) { + Contract.Assume(tokenReader != null); // BCL contract should say XmlReader.Create result != null + if (IsEncrypted(tokenReader)) { + Logger.InfoCard.DebugFormat("Incoming SAML token, before decryption: {0}", tokenXml); + decryptedBytes = decryptor.DecryptToken(tokenReader); + decryptedString = Encoding.UTF8.GetString(decryptedBytes); + Contract.Assume(decryptedString != null); // BCL contracts should be enhanced here + } else { + decryptedBytes = Encoding.UTF8.GetBytes(tokenXml); + decryptedString = tokenXml; + } } } diff --git a/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs b/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs index 48b7794..4ac871a 100644 --- a/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs +++ b/src/DotNetOpenAuth/InfoCard/Token/TokenUtility.cs @@ -226,7 +226,9 @@ namespace DotNetOpenAuth.InfoCard { int charMapLength = charMap.Length; byte[] raw = Convert.FromBase64String(ppid); - raw = SHA1.Create().ComputeHash(raw); + using (HashAlgorithm hasher = SHA1.Create()) { + raw = hasher.ComputeHash(raw); + } StringBuilder callSign = new StringBuilder(); diff --git a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs index dd34d90..c9bc1d3 100644 --- a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs @@ -90,11 +90,16 @@ namespace DotNetOpenAuth.Messaging { public override StreamReader GetResponseReader() { this.ResponseStream.Seek(0, SeekOrigin.Begin); string contentEncoding = this.Headers[HttpResponseHeader.ContentEncoding]; - if (string.IsNullOrEmpty(contentEncoding)) { - return new StreamReader(this.ResponseStream); - } else { - return new StreamReader(this.ResponseStream, Encoding.GetEncoding(contentEncoding)); + Encoding encoding = null; + if (!string.IsNullOrEmpty(contentEncoding)) { + try { + encoding = Encoding.GetEncoding(contentEncoding); + } catch (ArgumentException ex) { + Logger.Messaging.ErrorFormat("Encoding.GetEncoding(\"{0}\") threw ArgumentException: {1}", contentEncoding, ex); + } } + + return encoding != null ? new StreamReader(this.ResponseStream, encoding) : new StreamReader(this.ResponseStream); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs index df17dcb..03a8e6c 100644 --- a/src/DotNetOpenAuth/Messaging/Channel.cs +++ b/src/DotNetOpenAuth/Messaging/Channel.cs @@ -696,27 +696,28 @@ namespace DotNetOpenAuth.Messaging { WebHeaderCollection headers = new WebHeaderCollection(); headers.Add(HttpResponseHeader.ContentType, "text/html"); - StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture); - StringBuilder hiddenFields = new StringBuilder(); - foreach (var field in fields) { - hiddenFields.AppendFormat( - "\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n", - HttpUtility.HtmlEncode(field.Key), - HttpUtility.HtmlEncode(field.Value)); + using (StringWriter bodyWriter = new StringWriter(CultureInfo.InvariantCulture)) { + StringBuilder hiddenFields = new StringBuilder(); + foreach (var field in fields) { + hiddenFields.AppendFormat( + "\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n", + HttpUtility.HtmlEncode(field.Key), + HttpUtility.HtmlEncode(field.Value)); + } + bodyWriter.WriteLine( + IndirectMessageFormPostFormat, + HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), + hiddenFields); + bodyWriter.Flush(); + OutgoingWebResponse response = new OutgoingWebResponse { + Status = HttpStatusCode.OK, + Headers = headers, + Body = bodyWriter.ToString(), + OriginalMessage = message + }; + + return response; } - bodyWriter.WriteLine( - IndirectMessageFormPostFormat, - HttpUtility.HtmlEncode(message.Recipient.AbsoluteUri), - hiddenFields); - bodyWriter.Flush(); - OutgoingWebResponse response = new OutgoingWebResponse { - Status = HttpStatusCode.OK, - Headers = headers, - Body = bodyWriter.ToString(), - OriginalMessage = message - }; - - return response; } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs index ea3bf6b..1be62f5 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30128.0 +// Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -394,6 +394,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to An HttpContext.Current.Session object is required.. + /// </summary> + internal static string SessionRequired { + get { + return ResourceManager.GetString("SessionRequired", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Message signature was incorrect.. /// </summary> internal static string SignatureInvalid { diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx index 8b50767..cb80442 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx @@ -300,7 +300,10 @@ <data name="BinaryDataRequiresMultipart" xml:space="preserve"> <value>Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.</value> </data> + <data name="SessionRequired" xml:space="preserve"> + <value>An HttpContext.Current.Session object is required.</value> + </data> <data name="StandardMessageFactoryUnsupportedMessageType" xml:space="preserve"> <value>This message factory does not support message type(s): {0}</value> </data> -</root>
\ No newline at end of file +</root> diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index a52f51b..231637a 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -89,7 +89,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Transforms an OutgoingWebResponse to an MVC-friendly ActionResult. /// </summary> - /// <param name="response">The response to send to the uesr agent.</param> + /// <param name="response">The response to send to the user agent.</param> /// <returns>The <see cref="ActionResult"/> instance to be returned by the Controller's action method.</returns> public static ActionResult AsActionResult(this OutgoingWebResponse response) { Contract.Requires<ArgumentNullException>(response != null); @@ -176,6 +176,23 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Flattens the specified sequence of sequences. + /// </summary> + /// <typeparam name="T">The type of element contained in the sequence.</typeparam> + /// <param name="sequence">The sequence of sequences to flatten.</param> + /// <returns>A sequence of the contained items.</returns> + [Obsolete("Use Enumerable.SelectMany instead.")] + public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> sequence) { + ErrorUtilities.VerifyArgumentNotNull(sequence, "sequence"); + + foreach (IEnumerable<T> subsequence in sequence) { + foreach (T item in subsequence) { + yield return item; + } + } + } + + /// <summary> /// Sends a multipart HTTP POST request (useful for posting files) but doesn't call GetResponse on it. /// </summary> /// <param name="request">The HTTP request.</param> diff --git a/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs index cf22bb2..cc655cf 100644 --- a/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/OutgoingWebResponse.cs @@ -121,7 +121,7 @@ namespace DotNetOpenAuth.Messaging { /// Automatically sends the appropriate response to the user agent /// and ends execution on the current page or handler. /// </summary> - /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> + /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> /// <remarks> /// Requires a current HttpContext. /// </remarks> @@ -137,7 +137,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="context">The context of the HTTP request whose response should be set. /// Typically this is <see cref="HttpContext.Current"/>.</param> - /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> + /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> public virtual void Send(HttpContext context) { Contract.Requires<ArgumentNullException>(context != null); @@ -183,7 +183,10 @@ namespace DotNetOpenAuth.Messaging { /// would transmit the message that normally would be transmitted via a user agent redirect. /// </summary> /// <param name="channel">The channel to use for encoding.</param> - /// <returns>The URL that would transmit the original message.</returns> + /// <returns> + /// The URL that would transmit the original message. This URL may exceed the normal 2K limit, + /// and should therefore be broken up manually and POSTed as form fields when it exceeds this length. + /// </returns> /// <remarks> /// This is useful for desktop applications that will spawn a user agent to transmit the message /// rather than cause a redirect. diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs index 08e2411..ae54c3a 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs @@ -73,10 +73,10 @@ namespace DotNetOpenAuth.Messaging.Reflection { Contract.Assume(str != null); return bool.Parse(str); }; - Func<string, Identifier> safeIdentfier = str => { + Func<string, Identifier> safeIdentifier = str => { Contract.Assume(str != null); ErrorUtilities.VerifyFormat(str.Length > 0, MessagingStrings.NonEmptyStringExpected); - return Identifier.Parse(str); + return Identifier.Parse(str, true); }; Func<byte[], string> safeFromByteArray = bytes => { Contract.Assume(bytes != null); @@ -94,7 +94,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { Map<DateTime>(dt => XmlConvert.ToString(dt, XmlDateTimeSerializationMode.Utc), str => XmlConvert.ToDateTime(str, XmlDateTimeSerializationMode.Utc)); Map<byte[]>(safeFromByteArray, safeToByteArray); Map<Realm>(realm => realm.ToString(), safeRealm); - Map<Identifier>(id => id.ToString(), safeIdentfier); + Map<Identifier>(id => id.SerializedString, safeIdentifier); Map<bool>(value => value.ToString().ToLowerInvariant(), safeBool); Map<CultureInfo>(c => c.Name, str => new CultureInfo(str)); Map<CultureInfo[]>(cs => string.Join(",", cs.Select(c => c.Name).ToArray()), str => str.Split(',').Select(s => new CultureInfo(s)).ToArray()); diff --git a/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset b/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset index cee6f53..db238b6 100644 --- a/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset +++ b/src/DotNetOpenAuth/Migrated rules for DotNetOpenAuth.ruleset @@ -5,5 +5,6 @@ <Rule Id="CA1054" Action="None" /> <Rule Id="CA1055" Action="None" /> <Rule Id="CA1056" Action="None" /> + <Rule Id="CA2104" Action="None" /> </Rules> </RuleSet>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs b/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs new file mode 100644 index 0000000..9956966 --- /dev/null +++ b/src/DotNetOpenAuth/Mvc/OpenIdAjaxOptions.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdAjaxOptions.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Mvc { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// A set of customizations available for the scripts sent to the browser in AJAX OpenID scenarios. + /// </summary> + public class OpenIdAjaxOptions { + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdAjaxOptions"/> class. + /// </summary> + public OpenIdAjaxOptions() { + this.AssertionHiddenFieldId = "openid_openidAuthData"; + this.ReturnUrlHiddenFieldId = "ReturnUrl"; + } + + /// <summary> + /// Gets or sets the ID of the hidden field that should carry the positive assertion + /// until it is posted to the RP. + /// </summary> + public string AssertionHiddenFieldId { get; set; } + + /// <summary> + /// Gets or sets the ID of the hidden field that should be set with the parent window/frame's URL + /// prior to posting the form with the positive assertion. Useful for jQuery popup dialogs. + /// </summary> + public string ReturnUrlHiddenFieldId { get; set; } + + /// <summary> + /// Gets or sets the index of the form in the document.forms array on the browser that should + /// be submitted when the user is ready to send the positive assertion to the RP. + /// </summary> + public int FormIndex { get; set; } + + /// <summary> + /// Gets or sets the preloaded discovery results. + /// </summary> + public string PreloadedDiscoveryResults { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to print diagnostic trace messages in the browser. + /// </summary> + public bool ShowDiagnosticTrace { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether to show all the "hidden" iframes that facilitate + /// asynchronous authentication of the user for diagnostic purposes. + /// </summary> + public bool ShowDiagnosticIFrame { get; set; } + } +} diff --git a/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs b/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs new file mode 100644 index 0000000..f276019 --- /dev/null +++ b/src/DotNetOpenAuth/Mvc/OpenIdHelper.cs @@ -0,0 +1,432 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdHelper.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Mvc { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text; + using System.Web; + using System.Web.Mvc; + using System.Web.Routing; + using System.Web.UI; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Methods that generate HTML or Javascript for hosting AJAX OpenID "controls" on + /// ASP.NET MVC web sites. + /// </summary> + public static class OpenIdHelper { + /// <summary> + /// Emits a series of stylesheet import tags to support the AJAX OpenID Selector. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <returns>HTML that should be sent directly to the browser.</returns> + public static string OpenIdSelectorStyles(this HtmlHelper html, Page page) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Ensures(Contract.Result<string>() != null); + + StringWriter result = new StringWriter(); + result.WriteStylesheetLink(page, OpenId.RelyingParty.OpenIdSelector.EmbeddedStylesheetResourceName); + result.WriteStylesheetLink(page, OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedStylesheetResourceName); + return result.ToString(); + } + + /// <summary> + /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <returns>HTML that should be sent directly to the browser.</returns> + public static string OpenIdSelectorScripts(this HtmlHelper html, Page page) { + return OpenIdSelectorScripts(html, page, null, null); + } + + /// <summary> + /// Emits a series of script import tags and some inline script to support the AJAX OpenID Selector. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="selectorOptions">An optional instance of an <see cref="OpenIdSelector"/> control, whose properties have been customized to express how this MVC control should be rendered.</param> + /// <param name="additionalOptions">An optional set of additional script customizations.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelectorScripts(this HtmlHelper html, Page page, OpenIdSelector selectorOptions, OpenIdAjaxOptions additionalOptions) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Ensures(Contract.Result<string>() != null); + + if (selectorOptions == null) { + selectorOptions = new OpenId.RelyingParty.OpenIdSelector(); + } + + if (additionalOptions == null) { + additionalOptions = new OpenIdAjaxOptions(); + } + + StringWriter result = new StringWriter(); + + if (additionalOptions.ShowDiagnosticIFrame || additionalOptions.ShowDiagnosticTrace) { + string scriptFormat = @"window.openid_visible_iframe = {0}; // causes the hidden iframe to show up +window.openid_trace = {1}; // causes lots of messages"; + result.WriteScriptBlock(string.Format( + CultureInfo.InvariantCulture, + scriptFormat, + additionalOptions.ShowDiagnosticIFrame ? "true" : "false", + additionalOptions.ShowDiagnosticTrace ? "true" : "false")); + } + var scriptResources = new[] { + OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, + OpenIdRelyingPartyAjaxControlBase.EmbeddedAjaxJavascriptResource, + OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, + }; + result.WriteScriptTags(page, scriptResources); + + if (selectorOptions.DownloadYahooUILibrary) { + result.WriteScriptTags(new[] { "https://ajax.googleapis.com/ajax/libs/yui/2.8.0r4/build/yuiloader/yuiloader-min.js" }); + } + + var blockBuilder = new StringWriter(); + if (selectorOptions.DownloadYahooUILibrary) { + blockBuilder.WriteLine(@" try { + if (YAHOO) { + var loader = new YAHOO.util.YUILoader({ + require: ['button', 'menu'], + loadOptional: false, + combine: true + }); + + loader.insert(); + } + } catch (e) { }"); + } + + blockBuilder.WriteLine("window.aspnetapppath = '{0}';", VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath)); + + // Positive assertions can last no longer than this library is willing to consider them valid, + // and when they come with OP private associations they last no longer than the OP is willing + // to consider them valid. We assume the OP will hold them valid for at least five minutes. + double assertionLifetimeInMilliseconds = Math.Min(TimeSpan.FromMinutes(5).TotalMilliseconds, Math.Min(DotNetOpenAuthSection.Configuration.OpenId.MaxAuthenticationTime.TotalMilliseconds, DotNetOpenAuthSection.Configuration.Messaging.MaximumMessageLifetime.TotalMilliseconds)); + blockBuilder.WriteLine( + "{0} = {1};", + OpenIdRelyingPartyAjaxControlBase.MaxPositiveAssertionLifetimeJsName, + assertionLifetimeInMilliseconds.ToString(CultureInfo.InvariantCulture)); + + if (additionalOptions.PreloadedDiscoveryResults != null) { + blockBuilder.WriteLine(additionalOptions.PreloadedDiscoveryResults); + } + + string discoverUrl = VirtualPathUtility.AppendTrailingSlash(HttpContext.Current.Request.ApplicationPath) + html.RouteCollection["OpenIdDiscover"].GetVirtualPath(html.ViewContext.RequestContext, new RouteValueDictionary(new { identifier = "xxx" })).VirtualPath; + string blockFormat = @" {0} = function (argument, resultFunction, errorCallback) {{ + jQuery.ajax({{ + async: true, + dataType: 'text', + error: function (request, status, error) {{ errorCallback(status, argument); }}, + success: function (result) {{ resultFunction(result, argument); }}, + url: '{1}'.replace('xxx', encodeURIComponent(argument)) + }}); + }};"; + blockBuilder.WriteLine(blockFormat, OpenIdRelyingPartyAjaxControlBase.CallbackJSFunctionAsync, discoverUrl); + + blockFormat = @" window.postLoginAssertion = function (positiveAssertion) {{ + $('#{0}')[0].setAttribute('value', positiveAssertion); + if ($('#{1}')[0] && !$('#{1}')[0].value) {{ // popups have no ReturnUrl predefined, but full page LogOn does. + $('#{1}')[0].setAttribute('value', window.parent.location.href); + }} + document.forms[{2}].submit(); + }};"; + blockBuilder.WriteLine( + blockFormat, + additionalOptions.AssertionHiddenFieldId, + additionalOptions.ReturnUrlHiddenFieldId, + additionalOptions.FormIndex); + + blockFormat = @" $(function () {{ + var box = document.getElementsByName('openid_identifier')[0]; + initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, + null, // js function to invoke on receiving a positive assertion + {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, + false, // auto postback + null); // PostBackEventReference (unused in MVC) + }});"; + blockBuilder.WriteLine( + blockFormat, + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenIdTextBox.EmbeddedLogoResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)), + MessagingUtilities.GetSafeJavascriptValue(page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName)), + selectorOptions.Throttle, + selectorOptions.Timeout.TotalMilliseconds, + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnText), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnToolTip), + selectorOptions.TextBox.ShowLogOnPostBackButton ? "true" : "false", + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnPostBackToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryText), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.RetryToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.BusyToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.IdentifierRequiredMessage), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.LogOnInProgressMessage), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationSucceededToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticatedAsToolTip), + MessagingUtilities.GetSafeJavascriptValue(selectorOptions.TextBox.AuthenticationFailedToolTip)); + + result.WriteScriptBlock(blockBuilder.ToString()); + result.WriteScriptTags(page, OpenId.RelyingParty.OpenIdSelector.EmbeddedScriptResourceName); + + Reporting.RecordFeatureUse("MVC " + typeof(OpenIdSelector).Name); + return result.ToString(); + } + + /// <summary> + /// Emits the HTML to render an OpenID Provider button as a part of the overall OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="providerIdentifier">The OP Identifier.</param> + /// <param name="imageUrl">The URL of the image to display on the button.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelectorOPButton(this HtmlHelper html, Page page, Identifier providerIdentifier, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(providerIdentifier != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + return OpenIdSelectorButton(html, page, providerIdentifier, "OPButton", imageUrl); + } + + /// <summary> + /// Emits the HTML to render a generic OpenID button as a part of the overall OpenID Selector UI, + /// allowing the user to enter their own OpenID. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="imageUrl">The URL of the image to display on the button.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelectorOpenIdButton(this HtmlHelper html, Page page, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + return OpenIdSelectorButton(html, page, "OpenIDButton", "OpenIDButton", imageUrl); + } + + /// <summary> + /// Emits the HTML to render the entire OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="buttons">The buttons to include on the selector.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdSelector(this HtmlHelper html, Page page, params SelectorButton[] buttons) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(buttons != null); + Contract.Ensures(Contract.Result<string>() != null); + + var writer = new StringWriter(); + var h = new HtmlTextWriter(writer); + + h.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders"); + h.RenderBeginTag(HtmlTextWriterTag.Ul); + + foreach (SelectorButton button in buttons) { + var op = button as SelectorProviderButton; + if (op != null) { + h.Write(OpenIdSelectorOPButton(html, page, op.OPIdentifier, op.Image)); + continue; + } + + var openid = button as SelectorOpenIdButton; + if (openid != null) { + h.Write(OpenIdSelectorOpenIdButton(html, page, openid.Image)); + continue; + } + + ErrorUtilities.VerifySupported(false, "The {0} button is not yet supported for MVC.", button.GetType().Name); + } + + h.RenderEndTag(); // ul + + if (buttons.OfType<SelectorOpenIdButton>().Any()) { + h.Write(OpenIdAjaxTextBox(html)); + } + + return writer.ToString(); + } + + /// <summary> + /// Emits the HTML to render the <see cref="OpenIdAjaxTextBox"/> control as a part of the overall + /// OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + public static string OpenIdAjaxTextBox(this HtmlHelper html) { + return @"<div style='display: none' id='OpenIDForm'> + <span class='OpenIdAjaxTextBox' style='display: inline-block; position: relative; font-size: 16px'> + <input name='openid_identifier' id='openid_identifier' size='40' style='padding-left: 18px; border-style: solid; border-width: 1px; border-color: lightgray' /> + </span> + </div>"; + } + + /// <summary> + /// Emits the HTML to render a button as a part of the overall OpenID Selector UI. + /// </summary> + /// <param name="html">The <see cref="HtmlHelper"/> on the view.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="id">The value to assign to the HTML id attribute.</param> + /// <param name="cssClass">The value to assign to the HTML class attribute.</param> + /// <param name="imageUrl">The URL of the image to draw on the button.</param> + /// <returns> + /// HTML that should be sent directly to the browser. + /// </returns> + private static string OpenIdSelectorButton(this HtmlHelper html, Page page, string id, string cssClass, string imageUrl) { + Contract.Requires<ArgumentNullException>(html != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(id != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + Contract.Ensures(Contract.Result<string>() != null); + + var writer = new StringWriter(); + var h = new HtmlTextWriter(writer); + + h.AddAttribute(HtmlTextWriterAttribute.Id, id); + if (!string.IsNullOrEmpty(cssClass)) { + h.AddAttribute(HtmlTextWriterAttribute.Class, cssClass); + } + h.RenderBeginTag(HtmlTextWriterTag.Li); + + h.AddAttribute(HtmlTextWriterAttribute.Href, "#"); + h.RenderBeginTag(HtmlTextWriterTag.A); + + h.RenderBeginTag(HtmlTextWriterTag.Div); + h.RenderBeginTag(HtmlTextWriterTag.Div); + + h.AddAttribute(HtmlTextWriterAttribute.Src, imageUrl); + h.RenderBeginTag(HtmlTextWriterTag.Img); + h.RenderEndTag(); + + h.AddAttribute(HtmlTextWriterAttribute.Src, page.ClientScript.GetWebResourceUrl(typeof(OpenIdSelector), OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName)); + h.AddAttribute(HtmlTextWriterAttribute.Class, "loginSuccess"); + h.AddAttribute(HtmlTextWriterAttribute.Title, "Authenticated as {0}"); + h.RenderBeginTag(HtmlTextWriterTag.Img); + h.RenderEndTag(); + + h.RenderEndTag(); // div + + h.AddAttribute(HtmlTextWriterAttribute.Class, "ui-widget-overlay"); + h.RenderBeginTag(HtmlTextWriterTag.Div); + h.RenderEndTag(); // div + + h.RenderEndTag(); // div + h.RenderEndTag(); // a + h.RenderEndTag(); // li + + return writer.ToString(); + } + + /// <summary> + /// Emits <script> tags that import a given set of scripts given their URLs. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="scriptUrls">The locations of the scripts to import.</param> + private static void WriteScriptTags(this TextWriter writer, IEnumerable<string> scriptUrls) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(scriptUrls != null); + + foreach (string script in scriptUrls) { + writer.WriteLine("<script type='text/javascript' src='{0}'></script>", script); + } + } + + /// <summary> + /// Writes out script tags that import a script from resources embedded in this assembly. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="resourceName">Name of the resource.</param> + private static void WriteScriptTags(this TextWriter writer, Page page, string resourceName) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName)); + + WriteScriptTags(writer, page, new[] { resourceName }); + } + + /// <summary> + /// Writes out script tags that import scripts from resources embedded in this assembly. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="resourceNames">The resource names.</param> + private static void WriteScriptTags(this TextWriter writer, Page page, IEnumerable<string> resourceNames) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentNullException>(resourceNames != null); + + writer.WriteScriptTags(resourceNames.Select(r => page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyControlBase), r))); + } + + /// <summary> + /// Writes a given script block, surrounding it with <script> and CDATA tags. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="script">The script to inline on the page.</param> + private static void WriteScriptBlock(this TextWriter writer, string script) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(script)); + + writer.WriteLine("<script type='text/javascript' language='javascript'><!--"); + writer.WriteLine("//<![CDATA["); + writer.WriteLine(script); + writer.WriteLine("//]]>--></script>"); + } + + /// <summary> + /// Writes a given CSS link. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="page">The page being rendered.</param> + /// <param name="resourceName">Name of the resource containing the CSS content.</param> + private static void WriteStylesheetLink(this TextWriter writer, Page page, string resourceName) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentNullException>(page != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(resourceName)); + + WriteStylesheetLink(writer, page.ClientScript.GetWebResourceUrl(typeof(OpenIdRelyingPartyAjaxControlBase), resourceName)); + } + + /// <summary> + /// Writes a given CSS link. + /// </summary> + /// <param name="writer">The writer to emit the tags to.</param> + /// <param name="stylesheet">The stylesheet to link in.</param> + private static void WriteStylesheetLink(this TextWriter writer, string stylesheet) { + Contract.Requires<ArgumentNullException>(writer != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(stylesheet)); + + writer.WriteLine("<link rel='stylesheet' type='text/css' href='{0}' />", stylesheet); + } + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index ed41183..dc59b56 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -146,7 +146,11 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { ContentType contentType = new ContentType(request.Headers[HttpRequestHeader.ContentType]); if (string.Equals(contentType.MediaType, HttpFormUrlEncoded, StringComparison.Ordinal)) { foreach (string key in request.Form) { - fields.Add(key, request.Form[key]); + if (key != null) { + fields.Add(key, request.Form[key]); + } else { + Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.Form[key]); + } } } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs index b45da66..cf09036 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -258,6 +258,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>. /// </returns> protected virtual bool IsSignatureValid(ITamperResistantOAuthMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + string signature = this.GetSignature(message); return message.Signature == signature; } diff --git a/src/DotNetOpenAuth/OpenId/Association.cs b/src/DotNetOpenAuth/OpenId/Association.cs index 62e91ec..3c7e89f 100644 --- a/src/DotNetOpenAuth/OpenId/Association.cs +++ b/src/DotNetOpenAuth/OpenId/Association.cs @@ -238,24 +238,28 @@ namespace DotNetOpenAuth.OpenId { /// </returns> public override int GetHashCode() { HMACSHA1 hmac = new HMACSHA1(this.SecretKey); - CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); + try { + CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write); - byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); + byte[] hbytes = ASCIIEncoding.ASCII.GetBytes(this.Handle); - cs.Write(hbytes, 0, hbytes.Length); - cs.Close(); + cs.Write(hbytes, 0, hbytes.Length); + cs.Close(); - byte[] hash = hmac.Hash; - hmac.Clear(); + byte[] hash = hmac.Hash; + hmac.Clear(); - long val = 0; - for (int i = 0; i < hash.Length; i++) { - val = val ^ (long)hash[i]; - } + long val = 0; + for (int i = 0; i < hash.Length; i++) { + val = val ^ (long)hash[i]; + } - val = val ^ this.Expires.ToFileTimeUtc(); + val = val ^ this.Expires.ToFileTimeUtc(); - return (int)val; + return (int)val; + } finally { + ((IDisposable)hmac).Dispose(); + } } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/AssociationMemoryStore.cs b/src/DotNetOpenAuth/OpenId/AssociationMemoryStore.cs index 13633b4..7a20fd5 100644 --- a/src/DotNetOpenAuth/OpenId/AssociationMemoryStore.cs +++ b/src/DotNetOpenAuth/OpenId/AssociationMemoryStore.cs @@ -20,12 +20,22 @@ namespace DotNetOpenAuth.OpenId { /// </remarks> internal class AssociationMemoryStore<TKey> : IAssociationStore<TKey> { /// <summary> + /// How many association store requests should occur between each spring cleaning. + /// </summary> + private const int PeriodicCleaningFrequency = 10; + + /// <summary> /// For Relying Parties, this maps OP Endpoints to a set of associations with that endpoint. /// For Providers, this keeps smart and dumb associations in two distinct pools. /// </summary> private Dictionary<TKey, Associations> serverAssocsTable = new Dictionary<TKey, Associations>(); /// <summary> + /// A counter to track how close we are to an expired association cleaning run. + /// </summary> + private int periodicCleaning; + + /// <summary> /// Stores a given association for later recall. /// </summary> /// <param name="distinguishingFactor">The distinguishing factor, either an OP Endpoint or smart/dumb mode.</param> @@ -38,6 +48,13 @@ namespace DotNetOpenAuth.OpenId { Associations server_assocs = this.serverAssocsTable[distinguishingFactor]; server_assocs.Set(association); + + unchecked { + this.periodicCleaning++; + } + if (this.periodicCleaning % PeriodicCleaningFrequency == 0) { + this.ClearExpiredAssociations(); + } } } @@ -88,17 +105,6 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Clears all expired associations from the store. - /// </summary> - public void ClearExpiredAssociations() { - lock (this) { - foreach (Associations assocs in this.serverAssocsTable.Values) { - assocs.ClearExpired(); - } - } - } - - /// <summary> /// Gets the server associations for a given OP Endpoint or dumb/smart mode. /// </summary> /// <param name="distinguishingFactor">The distinguishing factor, either an OP Endpoint (for relying parties) or smart/dumb (for providers).</param> @@ -112,5 +118,16 @@ namespace DotNetOpenAuth.OpenId { return this.serverAssocsTable[distinguishingFactor]; } } + + /// <summary> + /// Clears all expired associations from the store. + /// </summary> + private void ClearExpiredAssociations() { + lock (this) { + foreach (Associations assocs in this.serverAssocsTable.Values) { + assocs.ClearExpired(); + } + } + } } } diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs b/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs index 580bdfd..01b74a1 100644 --- a/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs +++ b/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs @@ -65,7 +65,10 @@ namespace DotNetOpenAuth.OpenId.Behaviors { /// without malfunctioning. /// </remarks> void IRelyingPartyBehavior.OnOutgoingAuthenticationRequest(RelyingParty.IAuthenticationRequest request) { - request.SpreadSregToAX(AXFormats); + // Don't create AX extensions for OpenID 1.x messages, since AX requires OpenID 2.0. + if (request.Provider.Version.Major >= 2) { + request.SpreadSregToAX(AXFormats); + } } /// <summary> @@ -112,12 +115,7 @@ namespace DotNetOpenAuth.OpenId.Behaviors { bool IProviderBehavior.OnIncomingRequest(IRequest request) { var extensionRequest = request as Provider.HostProcessedRequest; if (extensionRequest != null) { - if (extensionRequest.GetExtension<ClaimsRequest>() == null) { - ClaimsRequest sreg = extensionRequest.UnifyExtensionsAsSreg(); - if (sreg != null) { - ((IProtocolMessageWithExtensions)extensionRequest.RequestMessage).Extensions.Add(sreg); - } - } + extensionRequest.UnifyExtensionsAsSreg(); } return false; diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs index 43d6c03..370192a 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ReturnToNonceBindingElement.cs @@ -209,7 +209,8 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// or if unsolicited assertions should be rejected at the RP; otherwise <c>false</c>. /// </returns> private bool UseRequestNonce(IMessage message) { - return message != null && (message.Version.Major < 2 || this.securitySettings.RejectUnsolicitedAssertions); + return message != null && (this.securitySettings.RejectUnsolicitedAssertions || + (message.Version.Major < 2 && this.securitySettings.ProtectDownlevelReplayAttacks)); } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs b/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs index 9e7ccd2..1b58c2f 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/ExtensionsInteropHelper.cs @@ -130,7 +130,8 @@ namespace DotNetOpenAuth.OpenId.Extensions { /// <summary> /// Looks for Simple Registration and Attribute Exchange (all known formats) - /// request extensions and returns them as a Simple Registration extension. + /// request extensions and returns them as a Simple Registration extension, + /// and adds the new extension to the original request message if it was absent. /// </summary> /// <param name="request">The authentication request.</param> /// <returns> @@ -140,7 +141,7 @@ namespace DotNetOpenAuth.OpenId.Extensions { internal static ClaimsRequest UnifyExtensionsAsSreg(this Provider.IHostProcessedRequest request) { Contract.Requires<ArgumentNullException>(request != null); - var req = (Provider.AuthenticationRequest)request; + var req = (Provider.HostProcessedRequest)request; var sreg = req.GetExtension<ClaimsRequest>(); if (sreg != null) { return sreg; @@ -148,7 +149,7 @@ namespace DotNetOpenAuth.OpenId.Extensions { var ax = req.GetExtension<FetchRequest>(); if (ax != null) { - sreg = new ClaimsRequest(); + sreg = new ClaimsRequest(SimpleRegistration.Constants.sreg_ns); sreg.Synthesized = true; ((IProtocolMessageWithExtensions)req.RequestMessage).Extensions.Add(sreg); sreg.BirthDate = GetDemandLevelFor(ax, WellKnownAttributes.BirthDate.WholeBirthDate); @@ -176,9 +177,9 @@ namespace DotNetOpenAuth.OpenId.Extensions { /// </remarks> internal static void ConvertSregToMatchRequest(this Provider.IHostProcessedRequest request) { var req = (Provider.HostProcessedRequest)request; - var response = (IProtocolMessageWithExtensions)req.Response; + var response = req.Response as IProtocolMessageWithExtensions; // negative responses don't support extensions. var sregRequest = request.GetExtension<ClaimsRequest>(); - if (sregRequest != null) { + if (sregRequest != null && response != null) { if (sregRequest.Synthesized) { var axRequest = request.GetExtension<FetchRequest>(); ErrorUtilities.VerifyInternal(axRequest != null, "How do we have a synthesized Sreg request without an AX request?"); diff --git a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs index 76a1c9b..f178647 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs @@ -61,6 +61,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { /// </summary> public UIRequest() { this.LanguagePreference = new[] { CultureInfo.CurrentUICulture }; + this.Mode = UIModes.Popup; } /// <summary> @@ -75,12 +76,11 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { public CultureInfo[] LanguagePreference { get; set; } /// <summary> - /// Gets the style of UI that the RP is hosting the OP's authentication page in. + /// Gets or sets the style of UI that the RP is hosting the OP's authentication page in. /// </summary> /// <value>Some value from the <see cref="UIModes"/> class. Defaults to <see cref="UIModes.Popup"/>.</value> - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Design is to allow this later to be changable when more than one value exists.")] [MessagePart("mode", AllowEmpty = false, IsRequired = true)] - public string Mode { get { return UIModes.Popup; } } + public string Mode { get; set; } /// <summary> /// Gets or sets a value indicating whether the Relying Party has an icon diff --git a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs index e96f362..ba9852e 100644 --- a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs +++ b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs @@ -110,29 +110,29 @@ namespace DotNetOpenAuth.OpenId { var results = new List<IdentifierDiscoveryResult>(); string signingHost; - var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost); - - if (response != null) { - try { - var document = new XrdsDocument(XmlReader.Create(response.ResponseStream)); - ValidateXmlDSig(document, uriIdentifier, response, signingHost); - var xrds = GetXrdElements(document, uriIdentifier.Uri.Host); - - // Look for claimed identifier template URIs for an additional XRDS document. - results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler)); - - // If we couldn't find any claimed identifiers, look for OP identifiers. - // Normally this would be the opposite (OP Identifiers take precedence over - // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers - // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers, - // which would break positive assertion checks). - if (results.Count == 0) { - results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier)); + using (var response = GetXrdsResponse(uriIdentifier, requestHandler, out signingHost)) { + if (response != null) { + try { + var document = new XrdsDocument(XmlReader.Create(response.ResponseStream)); + ValidateXmlDSig(document, uriIdentifier, response, signingHost); + var xrds = GetXrdElements(document, uriIdentifier.Uri.Host); + + // Look for claimed identifier template URIs for an additional XRDS document. + results.AddRange(GetExternalServices(xrds, uriIdentifier, requestHandler)); + + // If we couldn't find any claimed identifiers, look for OP identifiers. + // Normally this would be the opposite (OP Identifiers take precedence over + // claimed identifiers, but for Google Apps, XRDS' always have OP Identifiers + // mixed in, which the OpenID spec mandate should eclipse Claimed Identifiers, + // which would break positive assertion checks). + if (results.Count == 0) { + results.AddRange(xrds.CreateServiceEndpoints(uriIdentifier, uriIdentifier)); + } + + abortDiscoveryChain = true; + } catch (XmlException ex) { + Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex); } - - abortDiscoveryChain = true; - } catch (XmlException ex) { - Logger.Yadis.ErrorFormat("Error while parsing XRDS document at {0} pointed to by host-meta: {1}", response.FinalUri, ex); } } @@ -188,10 +188,11 @@ namespace DotNetOpenAuth.OpenId { Uri externalLocation = new Uri(templateNode.Value.Trim().Replace("{%uri}", Uri.EscapeDataString(identifier.Uri.AbsoluteUri))); string nextAuthority = nextAuthorityNode != null ? nextAuthorityNode.Value.Trim() : identifier.Uri.Host; try { - var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation); - XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream)); - ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority); - results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier)); + using (var externalXrdsResponse = GetXrdsResponse(identifier, requestHandler, externalLocation)) { + XrdsDocument externalXrds = new XrdsDocument(XmlReader.Create(externalXrdsResponse.ResponseStream)); + ValidateXmlDSig(externalXrds, identifier, externalXrdsResponse, nextAuthority); + results.AddRange(GetXrdElements(externalXrds, identifier).CreateServiceEndpoints(identifier, identifier)); + } } catch (ProtocolException ex) { Logger.Yadis.WarnFormat("HTTP GET error while retrieving described-by XRDS document {0}: {1}", externalLocation.AbsoluteUri, ex); } catch (XmlException ex) { @@ -217,9 +218,9 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentNullException>(response != null); var signatureNode = document.Node.SelectSingleNode("/xrds:XRDS/ds:Signature", document.XmlNamespaceResolver); - ErrorUtilities.VerifyProtocol(signatureNode != null, "Missing Signature element."); + ErrorUtilities.VerifyProtocol(signatureNode != null, OpenIdStrings.MissingElement, "Signature"); var signedInfoNode = signatureNode.SelectSingleNode("ds:SignedInfo", document.XmlNamespaceResolver); - ErrorUtilities.VerifyProtocol(signedInfoNode != null, "Missing SignedInfo element."); + ErrorUtilities.VerifyProtocol(signedInfoNode != null, OpenIdStrings.MissingElement, "SignedInfo"); ErrorUtilities.VerifyProtocol( signedInfoNode.SelectSingleNode("ds:CanonicalizationMethod[@Algorithm='http://docs.oasis-open.org/xri/xrd/2009/01#canonicalize-raw-octets']", document.XmlNamespaceResolver) != null, "Unrecognized or missing canonicalization method."); @@ -227,16 +228,19 @@ namespace DotNetOpenAuth.OpenId { signedInfoNode.SelectSingleNode("ds:SignatureMethod[@Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1']", document.XmlNamespaceResolver) != null, "Unrecognized or missing signature method."); var certNodes = signatureNode.Select("ds:KeyInfo/ds:X509Data/ds:X509Certificate", document.XmlNamespaceResolver); - ErrorUtilities.VerifyProtocol(certNodes.Count > 0, "Missing X509Certificate element."); + ErrorUtilities.VerifyProtocol(certNodes.Count > 0, OpenIdStrings.MissingElement, "X509Certificate"); var certs = certNodes.Cast<XPathNavigator>().Select(n => new X509Certificate2(Convert.FromBase64String(n.Value.Trim()))).ToList(); // Verify that we trust the signer of the certificates. // Start by trying to validate just the certificate used to sign the XRDS document, // since we can do that with partial trust. + Logger.OpenId.Debug("Verifying that we trust the certificate used to sign the discovery document."); if (!certs[0].Verify()) { // We couldn't verify just the signing certificate, so try to verify the whole certificate chain. try { + Logger.OpenId.Debug("Verifying the whole certificate chain."); VerifyCertChain(certs); + Logger.OpenId.Debug("Certificate chain verified."); } catch (SecurityException) { Logger.Yadis.Warn("Signing certificate verification failed and we have insufficient code access security permissions to perform certificate chain validation."); ErrorUtilities.ThrowProtocol(OpenIdStrings.X509CertificateNotTrusted); @@ -303,7 +307,7 @@ namespace DotNetOpenAuth.OpenId { request.CachePolicy = Yadis.IdentifierDiscoveryCachePolicy; request.Accept = ContentTypes.Xrds; var options = identifier.IsDiscoverySecureEndToEnd ? DirectWebRequestOptions.RequireSsl : DirectWebRequestOptions.None; - var response = requestHandler.GetResponse(request, options); + var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan); if (!string.Equals(response.ContentType.MediaType, ContentTypes.Xrds, StringComparison.Ordinal)) { Logger.Yadis.WarnFormat("Host-meta pointed to XRDS at {0}, but Content-Type at that URL was unexpected value '{1}'.", xrdsLocation, response.ContentType); } @@ -342,23 +346,24 @@ namespace DotNetOpenAuth.OpenId { private Uri GetXrdsLocation(UriIdentifier identifier, IDirectWebRequestHandler requestHandler, out string signingHost) { Contract.Requires<ArgumentNullException>(identifier != null); Contract.Requires<ArgumentNullException>(requestHandler != null); - var hostMetaResponse = this.GetHostMeta(identifier, requestHandler, out signingHost); - if (hostMetaResponse == null) { - return null; - } + using (var hostMetaResponse = this.GetHostMeta(identifier, requestHandler, out signingHost)) { + if (hostMetaResponse == null) { + return null; + } - using (var sr = hostMetaResponse.GetResponseReader()) { - string line = sr.ReadLine(); - Match m = HostMetaLink.Match(line); - if (m.Success) { - Uri location = new Uri(m.Groups["location"].Value); - Logger.Yadis.InfoFormat("Found link to XRDS at {0} in host-meta document {1}.", location, hostMetaResponse.FinalUri); - return location; + using (var sr = hostMetaResponse.GetResponseReader()) { + string line = sr.ReadLine(); + Match m = HostMetaLink.Match(line); + if (m.Success) { + Uri location = new Uri(m.Groups["location"].Value); + Logger.Yadis.InfoFormat("Found link to XRDS at {0} in host-meta document {1}.", location, hostMetaResponse.FinalUri); + return location; + } } - } - Logger.Yadis.WarnFormat("Could not find link to XRDS in host-meta document: {0}", hostMetaResponse.FinalUri); - return null; + Logger.Yadis.WarnFormat("Could not find link to XRDS in host-meta document: {0}", hostMetaResponse.FinalUri); + return null; + } } /// <summary> @@ -381,13 +386,19 @@ namespace DotNetOpenAuth.OpenId { if (identifier.IsDiscoverySecureEndToEnd) { options |= DirectWebRequestOptions.RequireSsl; } - var response = requestHandler.GetResponse(request, options); - if (response.Status == HttpStatusCode.OK) { - Logger.Yadis.InfoFormat("Found host-meta for {0} at: {1}", identifier.Uri.Host, hostMetaLocation); - signingHost = hostMetaProxy.GetSigningHost(identifier); - return response; - } else { - Logger.Yadis.InfoFormat("Could not obtain host-meta for {0} from {1}", identifier.Uri.Host, hostMetaLocation); + var response = requestHandler.GetResponse(request, options).GetSnapshot(Yadis.MaximumResultToScan); + try { + if (response.Status == HttpStatusCode.OK) { + Logger.Yadis.InfoFormat("Found host-meta for {0} at: {1}", identifier.Uri.Host, hostMetaLocation); + signingHost = hostMetaProxy.GetSigningHost(identifier); + return response; + } else { + Logger.Yadis.InfoFormat("Could not obtain host-meta for {0} from {1}", identifier.Uri.Host, hostMetaLocation); + response.Dispose(); + } + } catch { + response.Dispose(); + throw; } } diff --git a/src/DotNetOpenAuth/OpenId/IAssociationStore.cs b/src/DotNetOpenAuth/OpenId/IAssociationStore.cs index 71d8652..fb4487c 100644 --- a/src/DotNetOpenAuth/OpenId/IAssociationStore.cs +++ b/src/DotNetOpenAuth/OpenId/IAssociationStore.cs @@ -36,6 +36,12 @@ namespace DotNetOpenAuth.OpenId { /// <see cref="System.Uri"/> for consumers (to distinguish associations across servers) or /// <see cref="AssociationRelyingPartyType"/> for providers (to distinguish dumb and smart client associations). /// </typeparam> + /// <remarks> + /// Expired associations should be periodically cleared out of an association store. + /// This should be done frequently enough to avoid a memory leak, but sparingly enough + /// to not be a performance drain. Because this balance can vary by host, it is the + /// responsibility of the host to initiate this cleaning. + /// </remarks> ////[ContractClass(typeof(IAssociationStoreContract<>))] public interface IAssociationStore<TKey> { /// <summary> @@ -84,17 +90,6 @@ namespace DotNetOpenAuth.OpenId { /// before this call. /// </remarks> bool RemoveAssociation(TKey distinguishingFactor, string handle); - - /// <summary> - /// Clears all expired associations from the store. - /// </summary> - /// <remarks> - /// If another algorithm is in place to periodically clear out expired associations, - /// this method call may be ignored. - /// This should be done frequently enough to avoid a memory leak, but sparingly enough - /// to not be a performance drain. - /// </remarks> - void ClearExpiredAssociations(); } // For some odd reason, having this next class causes our test project to fail to build with this error: diff --git a/src/DotNetOpenAuth/OpenId/Identifier.cs b/src/DotNetOpenAuth/OpenId/Identifier.cs index 2ab5360..36ec784 100644 --- a/src/DotNetOpenAuth/OpenId/Identifier.cs +++ b/src/DotNetOpenAuth/OpenId/Identifier.cs @@ -39,6 +39,18 @@ namespace DotNetOpenAuth.OpenId { public string OriginalString { get; private set; } /// <summary> + /// Gets the Identifier in the form in which it should be serialized. + /// </summary> + /// <value> + /// For Identifiers that were originally deserialized, this is the exact same + /// string that was deserialized. For Identifiers instantiated in some other way, this is + /// the normalized form of the string used to instantiate the identifier. + /// </value> + internal virtual string SerializedString { + get { return this.IsDeserializedInstance ? this.OriginalString : this.ToString(); } + } + + /// <summary> /// Gets or sets a value indicating whether <see cref="Identifier"/> instances are considered equal /// based solely on their string reprsentations. /// </summary> @@ -59,6 +71,18 @@ namespace DotNetOpenAuth.OpenId { protected internal bool IsDiscoverySecureEndToEnd { get; private set; } /// <summary> + /// Gets a value indicating whether this instance was initialized from + /// deserializing a message. + /// </summary> + /// <remarks> + /// This is interesting because when an Identifier comes from the network, + /// we can't normalize it and then expect signatures to still verify. + /// But if the Identifier is initialized locally, we can and should normalize it + /// before serializing it. + /// </remarks> + protected bool IsDeserializedInstance { get; private set; } + + /// <summary> /// Converts the string representation of an Identifier to its strong type. /// </summary> /// <param name="identifier">The identifier.</param> @@ -118,11 +142,32 @@ namespace DotNetOpenAuth.OpenId { Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(identifier)); Contract.Ensures(Contract.Result<Identifier>() != null); + return Parse(identifier, false); + } + + /// <summary> + /// Parses an identifier string and automatically determines + /// whether it is an XRI or URI. + /// </summary> + /// <param name="identifier">Either a URI or XRI identifier.</param> + /// <param name="serializeExactValue">if set to <c>true</c> this Identifier will serialize exactly as given rather than in its normalized form.</param> + /// <returns> + /// An <see cref="Identifier"/> instance for the given value. + /// </returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Some of these identifiers are not properly formatted to be Uris at this stage.")] + public static Identifier Parse(string identifier, bool serializeExactValue) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(identifier)); + Contract.Ensures(Contract.Result<Identifier>() != null); + + Identifier id; if (XriIdentifier.IsValidXri(identifier)) { - return new XriIdentifier(identifier); + id = new XriIdentifier(identifier); } else { - return new UriIdentifier(identifier); + id = new UriIdentifier(identifier); } + + id.IsDeserializedInstance = serializeExactValue; + return id; } /// <summary> @@ -212,6 +257,19 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Reparses the specified identifier in order to be assured that the concrete type that + /// implements the identifier is one of the well-known ones. + /// </summary> + /// <param name="identifier">The identifier.</param> + /// <returns>Either <see cref="XriIdentifier"/> or <see cref="UriIdentifier"/>.</returns> + internal static Identifier Reparse(Identifier identifier) { + Contract.Requires<ArgumentNullException>(identifier != null); + Contract.Ensures(Contract.Result<Identifier>() != null); + + return Parse(identifier, identifier.IsDeserializedInstance); + } + + /// <summary> /// Returns an <see cref="Identifier"/> that has no URI fragment. /// Quietly returns the original <see cref="Identifier"/> if it is not /// a <see cref="UriIdentifier"/> or no fragment exists. diff --git a/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs b/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs index 3190920..c851f24 100644 --- a/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs +++ b/src/DotNetOpenAuth/OpenId/IdentifierDiscoveryResult.cs @@ -89,7 +89,7 @@ namespace DotNetOpenAuth.OpenId { // not a derived type that will override expected behavior. // Elsewhere in this class, we count on the fact that this property // is either UriIdentifier or XriIdentifier. MockIdentifier messes it up. - this.claimedIdentifier = value != null ? Identifier.Parse(value) : null; + this.claimedIdentifier = value != null ? Identifier.Reparse(value) : null; } } diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index 29315bb..43283ac 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30104.0 +// Runtime Version:4.0.30319.1 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -196,6 +196,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to This OpenID exploits features that this relying party cannot reliably verify. Please try logging in with a human-readable OpenID or from a different OpenID Provider.. + /// </summary> + internal static string ClaimedIdentifierDefiesDotNetNormalization { + get { + return ResourceManager.GetString("ClaimedIdentifierDefiesDotNetNormalization", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The ClaimedIdentifier property must be set first.. /// </summary> internal static string ClaimedIdentifierMustBeSetFirst { @@ -416,6 +425,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to Missing {0} element.. + /// </summary> + internal static string MissingElement { + get { + return ResourceManager.GetString("MissingElement", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to No recognized association type matches the requested length of {0}.. /// </summary> internal static string NoAssociationTypeFoundByLength { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index ae68fe6..fab03a9 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -349,4 +349,10 @@ Discovered endpoint info: <data name="X509CertificateNotTrusted" xml:space="preserve"> <value>The X.509 certificate used to sign this document is not trusted.</value> </data> + <data name="ClaimedIdentifierDefiesDotNetNormalization" xml:space="preserve"> + <value>This OpenID exploits features that this relying party cannot reliably verify. Please try logging in with a human-readable OpenID or from a different OpenID Provider.</value> + </data> + <data name="MissingElement" xml:space="preserve"> + <value>Missing {0} element.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs b/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs index 445978e..e792a81 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/ProviderEndpoint.cs @@ -98,11 +98,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </remarks> public static IAuthenticationRequest PendingAuthenticationRequest { get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); Contract.Ensures(Contract.Result<IAuthenticationRequest>() == null || PendingRequest != null); return HttpContext.Current.Session[PendingRequestKey] as IAuthenticationRequest; } set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); HttpContext.Current.Session[PendingRequestKey] = value; } } @@ -118,11 +122,15 @@ namespace DotNetOpenAuth.OpenId.Provider { /// </remarks> public static IAnonymousRequest PendingAnonymousRequest { get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); Contract.Ensures(Contract.Result<IAnonymousRequest>() == null || PendingRequest != null); return HttpContext.Current.Session[PendingRequestKey] as IAnonymousRequest; } set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); HttpContext.Current.Session[PendingRequestKey] = value; } } @@ -137,8 +145,17 @@ namespace DotNetOpenAuth.OpenId.Provider { /// before responding to the relying party's request. /// </remarks> public static IHostProcessedRequest PendingRequest { - get { return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest; } - set { HttpContext.Current.Session[PendingRequestKey] = value; } + get { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + return HttpContext.Current.Session[PendingRequestKey] as IHostProcessedRequest; + } + + set { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<InvalidOperationException>(HttpContext.Current.Session != null, MessagingStrings.SessionRequired); + HttpContext.Current.Session[PendingRequestKey] = value; + } } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs index f33a655..4fa2d64 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/StandardProviderApplicationStore.cs @@ -100,19 +100,6 @@ namespace DotNetOpenAuth.OpenId.Provider { return this.associationStore.RemoveAssociation(distinguishingFactor, handle); } - /// <summary> - /// Clears all expired associations from the store. - /// </summary> - /// <remarks> - /// If another algorithm is in place to periodically clear out expired associations, - /// this method call may be ignored. - /// This should be done frequently enough to avoid a memory leak, but sparingly enough - /// to not be a performance drain. - /// </remarks> - public void ClearExpiredAssociations() { - this.associationStore.ClearExpiredAssociations(); - } - #endregion #region INonceStore Members diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs index 7d30a3f..ac70387 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AssociationManager.cs @@ -10,6 +10,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Diagnostics.Contracts; using System.Linq; using System.Net; + using System.Security; using System.Text; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.ChannelElements; @@ -148,10 +149,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return null; } - var associateRequest = AssociateRequest.Create(this.securitySettings, provider); - - const int RenegotiateRetries = 1; - return this.CreateNewAssociation(provider, associateRequest, RenegotiateRetries); + try { + var associateRequest = AssociateRequest.Create(this.securitySettings, provider); + + const int RenegotiateRetries = 1; + return this.CreateNewAssociation(provider, associateRequest, RenegotiateRetries); + } catch (VerificationException ex) { + // See Trac ticket #163. In partial trust host environments, the + // Diffie-Hellman implementation we're using for HTTP OP endpoints + // sometimes causes the CLR to throw: + // "VerificationException: Operation could destabilize the runtime." + // Just give up and use dumb mode in this case. + Logger.OpenId.ErrorFormat("VerificationException occurred while trying to create an association with {0}. {1}", provider.Uri, ex); + return null; + } } /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs index e90c1d3..77d8ae1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs @@ -4,6 +4,8 @@ // </copyright> //----------------------------------------------------------------------- +using System.Threading; + namespace DotNetOpenAuth.OpenId.RelyingParty { using System; using System.Collections.Generic; @@ -297,6 +299,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <remarks> /// This method requires an ASP.NET HttpContext. /// </remarks> + /// <exception cref="ThreadAbortException">Typically thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> public void RedirectToProvider() { this.RedirectingResponse.Send(); } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs new file mode 100644 index 0000000..94eb5ba --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/DuplicateRequestedHostsComparer.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// <copyright file="DuplicateRequestedHostsComparer.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// An authentication request comparer that judges equality solely on the OP endpoint hostname. + /// </summary> + internal class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { + /// <summary> + /// The singleton instance of this comparer. + /// </summary> + private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); + + /// <summary> + /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. + /// </summary> + private DuplicateRequestedHostsComparer() { + } + + /// <summary> + /// Gets the singleton instance of this comparer. + /// </summary> + internal static IEqualityComparer<IAuthenticationRequest> Instance { + get { return instance; } + } + + #region IEqualityComparer<IAuthenticationRequest> Members + + /// <summary> + /// Determines whether the specified objects are equal. + /// </summary> + /// <param name="x">The first object to compare.</param> + /// <param name="y">The second object to compare.</param> + /// <returns> + /// true if the specified objects are equal; otherwise, false. + /// </returns> + public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { + if (x == null && y == null) { + return true; + } + + if (x == null || y == null) { + return false; + } + + // We'll distinguish based on the host name only, which + // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, + // this multiple OP attempt thing was just a convenience feature anyway. + return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); + } + + /// <summary> + /// Returns a hash code for the specified object. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> + /// <returns>A hash code for the specified object.</returns> + /// <exception cref="T:System.ArgumentNullException"> + /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. + /// </exception> + public int GetHashCode(IAuthenticationRequest obj) { + return obj.Provider.Uri.Host.GetHashCode(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs new file mode 100644 index 0000000..ae9fbdc --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxRelyingParty.cs @@ -0,0 +1,238 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdAjaxRelyingParty.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; + using System.Web; + using System.Web.Script.Serialization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Extensions; + using DotNetOpenAuth.OpenId.Extensions.UI; + + /// <summary> + /// Provides the programmatic facilities to act as an AJAX-enabled OpenID relying party. + /// </summary> + public class OpenIdAjaxRelyingParty : OpenIdRelyingParty { + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdAjaxRelyingParty"/> class. + /// </summary> + public OpenIdAjaxRelyingParty() { + Reporting.RecordFeatureUse(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdAjaxRelyingParty"/> class. + /// </summary> + /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param> + public OpenIdAjaxRelyingParty(IRelyingPartyApplicationStore applicationStore) + : base(applicationStore) { + Reporting.RecordFeatureUse(this); + } + + /// <summary> + /// Generates AJAX-ready authentication requests that can satisfy the requirements of some OpenID Identifier. + /// </summary> + /// <param name="userSuppliedIdentifier">The Identifier supplied by the user. This may be a URL, an XRI or i-name.</param> + /// <param name="realm">The shorest URL that describes this relying party web site's address. + /// For example, if your login page is found at https://www.example.com/login.aspx, + /// your realm would typically be https://www.example.com/.</param> + /// <param name="returnToUrl">The URL of the login page, or the page prepared to receive authentication + /// responses from the OpenID Provider.</param> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// <para>Any individual generated request can satisfy the authentication. + /// The generated requests are sorted in preferred order. + /// Each request is generated as it is enumerated to. Associations are created only as + /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para> + /// <para>No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead.</para> + /// </remarks> + public override IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + var requests = base.CreateRequests(userSuppliedIdentifier, realm, returnToUrl); + + // Alter the requests so that have AJAX characteristics. + // Some OPs may be listed multiple times (one with HTTPS and the other with HTTP, for example). + // Since we're gathering OPs to try one after the other, just take the first choice of each OP + // and don't try it multiple times. + requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance); + + // Configure each generated request. + int reqIndex = 0; + foreach (var req in requests) { + // Inform ourselves in return_to that we're in a popup. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyControlBase.UIPopupCallbackKey, "1"); + + if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) { + // Inform the OP that we'll be using a popup window consistent with the UI extension. + req.AddExtension(new UIRequest()); + + // Provide a hint for the client javascript about whether the OP supports the UI extension. + // This is so the window can be made the correct size for the extension. + // If the OP doesn't advertise support for the extension, the javascript will use + // a bigger popup window. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyControlBase.PopupUISupportedJSHint, "1"); + } + + req.SetUntrustedCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); + + // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter + if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { + req.SetUntrustedCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, userSuppliedIdentifier.OriginalString); + } + + // Our javascript needs to let the user know which endpoint responded. So we force it here. + // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); + + // Inform ourselves in return_to that we're in a popup or iframe. + req.SetUntrustedCallbackArgument(OpenIdRelyingPartyAjaxControlBase.UIPopupCallbackKey, "1"); + + // We append a # at the end so that if the OP happens to support it, + // the OpenID response "query string" is appended after the hash rather than before, resulting in the + // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version + // of the static resource down from the server merely because of a changed URL. + // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html + ////TODO: + + yield return req; + } + } + + /// <summary> + /// Serializes discovery results on some <i>single</i> identifier on behalf of Javascript running on the browser. + /// </summary> + /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param> + /// <returns> + /// The JSON result to return to the user agent. + /// </returns> + /// <remarks> + /// We prepare a JSON object with this interface: + /// <code> + /// class jsonResponse { + /// string claimedIdentifier; + /// Array requests; // never null + /// string error; // null if no error + /// } + /// </code> + /// Each element in the requests array looks like this: + /// <code> + /// class jsonAuthRequest { + /// string endpoint; // URL to the OP endpoint + /// string immediate; // URL to initiate an immediate request + /// string setup; // URL to initiate a setup request. + /// } + /// </code> + /// </remarks> + public OutgoingWebResponse AsAjaxDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + var serializer = new JavaScriptSerializer(); + return new OutgoingWebResponse { + Body = serializer.Serialize(this.AsJsonDiscoveryResult(requests)), + }; + } + + /// <summary> + /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries + /// an AJAX-aware OpenID control. + /// </summary> + /// <param name="requests">The discovery results to serialize as a JSON response.</param> + /// <returns> + /// The JSON result to return to the user agent. + /// </returns> + public string AsAjaxPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + var serializer = new JavaScriptSerializer(); + string json = serializer.Serialize(this.AsJsonPreloadedDiscoveryResult(requests)); + + string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + json + ");"; + return script; + } + + /// <summary> + /// Converts a sequence of authentication requests to a JSON object for seeding an AJAX-enabled login page. + /// </summary> + /// <param name="requests">The discovery results from just <i>one</i> identifier to serialize as a JSON response.</param> + /// <returns>A JSON object, not yet serialized.</returns> + internal object AsJsonDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + requests = requests.CacheGeneratedResults(); + + if (requests.Any()) { + return new { + claimedIdentifier = (string)requests.First().ClaimedIdentifier, + requests = requests.Select(req => new { + endpoint = req.Provider.Uri.AbsoluteUri, + immediate = this.GetRedirectUrl(req, true), + setup = this.GetRedirectUrl(req, false), + }).ToArray() + }; + } else { + return new { + requests = new object[0], + error = OpenIdStrings.OpenIdEndpointNotFound, + }; + } + } + + /// <summary> + /// Serializes discovery on a set of identifiers for preloading into an HTML page that carries + /// an AJAX-aware OpenID control. + /// </summary> + /// <param name="requests">The discovery results to serialize as a JSON response.</param> + /// <returns> + /// A JSON object, not yet serialized to a string. + /// </returns> + private object AsJsonPreloadedDiscoveryResult(IEnumerable<IAuthenticationRequest> requests) { + Contract.Requires<ArgumentNullException>(requests != null); + + // We prepare a JSON object with this interface: + // Array discoveryWrappers; + // Where each element in the above array has this interface: + // class discoveryWrapper { + // string userSuppliedIdentifier; + // jsonResponse discoveryResult; // contains result of call to SerializeDiscoveryAsJson(Identifier) + // } + var json = (from request in requests + group request by request.DiscoveryResult.UserSuppliedIdentifier into requestsByIdentifier + select new { + userSuppliedIdentifier = (string)requestsByIdentifier.Key, + discoveryResult = this.AsJsonDiscoveryResult(requestsByIdentifier), + }).ToArray(); + + return json; + } + + /// <summary> + /// Gets the full URL that carries an OpenID message, even if it exceeds the normal maximum size of a URL, + /// for purposes of sending to an AJAX component running in the browser. + /// </summary> + /// <param name="request">The authentication request.</param> + /// <param name="immediate"><c>true</c>to create a checkid_immediate request; + /// <c>false</c> to create a checkid_setup request.</param> + /// <returns>The absolute URL that carries the entire OpenID message.</returns> + private Uri GetRedirectUrl(IAuthenticationRequest request, bool immediate) { + Contract.Requires<ArgumentNullException>(request != null); + + request.Mode = immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; + return request.RedirectingResponse.GetDirectUriRequest(this.Channel); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs index 097d065..d80bf6a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -64,6 +64,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> internal const bool DownloadYahooUILibraryDefault = true; + /// <summary> + /// The default value for the <see cref="Throttle"/> property. + /// </summary> + internal const int ThrottleDefault = 3; + #region Property viewstate keys /// <summary> @@ -221,11 +226,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const string AuthenticationFailedToolTipDefault = "Authentication failed."; /// <summary> - /// The default value for the <see cref="Throttle"/> property. - /// </summary> - private const int ThrottleDefault = 3; - - /// <summary> /// The default value for the <see cref="LogOnText"/> property. /// </summary> private const string LogOnTextDefault = "LOG IN"; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs index c13c61c..4aa78a5 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdLogin.cs @@ -702,79 +702,104 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // top row, left cell cell = new TableCell(); - this.label = new HtmlGenericControl("label"); - this.label.InnerText = LabelTextDefault; - cell.Controls.Add(this.label); - row1.Cells.Add(cell); + try { + this.label = new HtmlGenericControl("label"); + this.label.InnerText = LabelTextDefault; + cell.Controls.Add(this.label); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // top row, middle cell cell = new TableCell(); - cell.Controls.Add(new InPlaceControl(this)); - row1.Cells.Add(cell); + try { + cell.Controls.Add(new InPlaceControl(this)); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // top row, right cell cell = new TableCell(); - this.loginButton = new Button(); - this.loginButton.ID = "loginButton"; - this.loginButton.Text = ButtonTextDefault; - this.loginButton.ToolTip = ButtonToolTipDefault; - this.loginButton.Click += this.LoginButton_Click; - this.loginButton.ValidationGroup = ValidationGroupDefault; + try { + this.loginButton = new Button(); + this.loginButton.ID = "loginButton"; + this.loginButton.Text = ButtonTextDefault; + this.loginButton.ToolTip = ButtonToolTipDefault; + this.loginButton.Click += this.LoginButton_Click; + this.loginButton.ValidationGroup = ValidationGroupDefault; #if !Mono - this.panel.DefaultButton = this.loginButton.ID; + this.panel.DefaultButton = this.loginButton.ID; #endif - cell.Controls.Add(this.loginButton); - row1.Cells.Add(cell); + cell.Controls.Add(this.loginButton); + row1.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // middle row, left cell row2.Cells.Add(new TableCell()); // middle row, middle cell cell = new TableCell(); - cell.Style[HtmlTextWriterStyle.Color] = "gray"; - cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; - this.requiredValidator = new RequiredFieldValidator(); - this.requiredValidator.ErrorMessage = RequiredTextDefault + RequiredTextSuffix; - this.requiredValidator.Text = RequiredTextDefault + RequiredTextSuffix; - this.requiredValidator.Display = ValidatorDisplay.Dynamic; - this.requiredValidator.ValidationGroup = ValidationGroupDefault; - cell.Controls.Add(this.requiredValidator); - this.identifierFormatValidator = new CustomValidator(); - this.identifierFormatValidator.ErrorMessage = UriFormatTextDefault + RequiredTextSuffix; - this.identifierFormatValidator.Text = UriFormatTextDefault + RequiredTextSuffix; - this.identifierFormatValidator.ServerValidate += this.IdentifierFormatValidator_ServerValidate; - this.identifierFormatValidator.Enabled = UriValidatorEnabledDefault; - this.identifierFormatValidator.Display = ValidatorDisplay.Dynamic; - this.identifierFormatValidator.ValidationGroup = ValidationGroupDefault; - cell.Controls.Add(this.identifierFormatValidator); - this.errorLabel = new Label(); - this.errorLabel.EnableViewState = false; - this.errorLabel.ForeColor = System.Drawing.Color.Red; - this.errorLabel.Style[HtmlTextWriterStyle.Display] = "block"; // puts it on its own line - this.errorLabel.Visible = false; - cell.Controls.Add(this.errorLabel); - this.examplePrefixLabel = new Label(); - this.examplePrefixLabel.Text = ExamplePrefixDefault; - cell.Controls.Add(this.examplePrefixLabel); - cell.Controls.Add(new LiteralControl(" ")); - this.exampleUrlLabel = new Label(); - this.exampleUrlLabel.Font.Bold = true; - this.exampleUrlLabel.Text = ExampleUrlDefault; - cell.Controls.Add(this.exampleUrlLabel); - row2.Cells.Add(cell); + try { + cell.Style[HtmlTextWriterStyle.Color] = "gray"; + cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; + this.requiredValidator = new RequiredFieldValidator(); + this.requiredValidator.ErrorMessage = RequiredTextDefault + RequiredTextSuffix; + this.requiredValidator.Text = RequiredTextDefault + RequiredTextSuffix; + this.requiredValidator.Display = ValidatorDisplay.Dynamic; + this.requiredValidator.ValidationGroup = ValidationGroupDefault; + cell.Controls.Add(this.requiredValidator); + this.identifierFormatValidator = new CustomValidator(); + this.identifierFormatValidator.ErrorMessage = UriFormatTextDefault + RequiredTextSuffix; + this.identifierFormatValidator.Text = UriFormatTextDefault + RequiredTextSuffix; + this.identifierFormatValidator.ServerValidate += this.IdentifierFormatValidator_ServerValidate; + this.identifierFormatValidator.Enabled = UriValidatorEnabledDefault; + this.identifierFormatValidator.Display = ValidatorDisplay.Dynamic; + this.identifierFormatValidator.ValidationGroup = ValidationGroupDefault; + cell.Controls.Add(this.identifierFormatValidator); + this.errorLabel = new Label(); + this.errorLabel.EnableViewState = false; + this.errorLabel.ForeColor = System.Drawing.Color.Red; + this.errorLabel.Style[HtmlTextWriterStyle.Display] = "block"; // puts it on its own line + this.errorLabel.Visible = false; + cell.Controls.Add(this.errorLabel); + this.examplePrefixLabel = new Label(); + this.examplePrefixLabel.Text = ExamplePrefixDefault; + cell.Controls.Add(this.examplePrefixLabel); + cell.Controls.Add(new LiteralControl(" ")); + this.exampleUrlLabel = new Label(); + this.exampleUrlLabel.Font.Bold = true; + this.exampleUrlLabel.Text = ExampleUrlDefault; + cell.Controls.Add(this.exampleUrlLabel); + row2.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // middle row, right cell cell = new TableCell(); - cell.Style[HtmlTextWriterStyle.Color] = "gray"; - cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; - cell.Style[HtmlTextWriterStyle.TextAlign] = "center"; - this.registerLink = new HyperLink(); - this.registerLink.Text = RegisterTextDefault; - this.registerLink.ToolTip = RegisterToolTipDefault; - this.registerLink.NavigateUrl = RegisterUrlDefault; - this.registerLink.Visible = RegisterVisibleDefault; - cell.Controls.Add(this.registerLink); - row2.Cells.Add(cell); + try { + cell.Style[HtmlTextWriterStyle.Color] = "gray"; + cell.Style[HtmlTextWriterStyle.FontSize] = "smaller"; + cell.Style[HtmlTextWriterStyle.TextAlign] = "center"; + this.registerLink = new HyperLink(); + this.registerLink.Text = RegisterTextDefault; + this.registerLink.ToolTip = RegisterToolTipDefault; + this.registerLink.NavigateUrl = RegisterUrlDefault; + this.registerLink.Visible = RegisterVisibleDefault; + cell.Controls.Add(this.registerLink); + row2.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // bottom row, left cell cell = new TableCell(); @@ -782,17 +807,27 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // bottom row, middle cell cell = new TableCell(); - this.rememberMeCheckBox = new CheckBox(); - this.rememberMeCheckBox.Text = RememberMeTextDefault; - this.rememberMeCheckBox.Checked = this.UsePersistentCookie != LogOnPersistence.Session; - this.rememberMeCheckBox.Visible = RememberMeVisibleDefault; - this.rememberMeCheckBox.CheckedChanged += this.RememberMeCheckBox_CheckedChanged; - cell.Controls.Add(this.rememberMeCheckBox); - row3.Cells.Add(cell); + try { + this.rememberMeCheckBox = new CheckBox(); + this.rememberMeCheckBox.Text = RememberMeTextDefault; + this.rememberMeCheckBox.Checked = this.UsePersistentCookie != LogOnPersistence.Session; + this.rememberMeCheckBox.Visible = RememberMeVisibleDefault; + this.rememberMeCheckBox.CheckedChanged += this.RememberMeCheckBox_CheckedChanged; + cell.Controls.Add(this.rememberMeCheckBox); + row3.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // bottom row, right cell cell = new TableCell(); - row3.Cells.Add(cell); + try { + row3.Cells.Add(cell); + } catch { + cell.Dispose(); + throw; + } // this sets all the controls' tab indexes this.TabIndex = TabIndexDefault; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs index dbf9530..8684bd1 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdMobileTextBox.cs @@ -762,13 +762,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { IRelyingPartyApplicationStore store = this.Stateless ? null : (this.CustomApplicationStore ?? DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore)); var rp = new OpenIdRelyingParty(store); - - // Only set RequireSsl to true, as we don't want to override - // a .config setting of true with false. - if (this.RequireSsl) { - rp.SecuritySettings.RequireSsl = true; + try { + // Only set RequireSsl to true, as we don't want to override + // a .config setting of true with false. + if (this.RequireSsl) { + rp.SecuritySettings.RequireSsl = true; + } + return rp; + } catch { + rp.Dispose(); + throw; } - return rp; } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index a0a0fb9..a416f3a 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -12,7 +12,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; + using System.Globalization; using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -33,10 +37,10 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public delegate bool EndpointSelector(IProviderEndpoint endpoint); /// <summary> - /// Provides the programmatic facilities to act as an OpenId consumer. + /// Provides the programmatic facilities to act as an OpenID relying party. /// </summary> [ContractVerification(true)] - public sealed class OpenIdRelyingParty : IDisposable { + public class OpenIdRelyingParty : IDisposable { /// <summary> /// The name of the key to use in the HttpApplication cache to store the /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use. @@ -54,6 +58,22 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private readonly IList<IIdentifierDiscoveryService> discoveryServices = new List<IIdentifierDiscoveryService>(2); /// <summary> + /// Backing field for the <see cref="NonVerifyingRelyingParty"/> property. + /// </summary> + private OpenIdRelyingParty nonVerifyingRelyingParty; + + /// <summary> + /// The lock to obtain when initializing the <see cref="nonVerifyingRelyingParty"/> member. + /// </summary> + private object nonVerifyingRelyingPartyInitLock = new object(); + + /// <summary> + /// A dictionary of extension response types and the javascript member + /// name to map them to on the user agent. + /// </summary> + private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); + + /// <summary> /// Backing field for the <see cref="SecuritySettings"/> property. /// </summary> private RelyingPartySecuritySettings securitySettings; @@ -78,7 +98,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class. /// </summary> - /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param> + /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param> public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore) : this(applicationStore, applicationStore) { } @@ -108,11 +128,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { // Without a nonce store, we must rely on the Provider to protect against // replay attacks. But only 2.0+ Providers can be expected to provide // replay protection. - if (nonceStore == null) { - if (this.SecuritySettings.MinimumRequiredOpenIdVersion < ProtocolVersion.V20) { - Logger.OpenId.Warn("Raising minimum OpenID version requirement for Providers to 2.0 to protect this stateless RP from replay attacks."); - this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; - } + if (nonceStore == null && + this.SecuritySettings.ProtectDownlevelReplayAttacks && + this.SecuritySettings.MinimumRequiredOpenIdVersion < ProtocolVersion.V20) { + Logger.OpenId.Warn("Raising minimum OpenID version requirement for Providers to 2.0 to protect this stateless RP from replay attacks."); + this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; } this.channel = new OpenIdChannel(associationStore, nonceStore, this.SecuritySettings); @@ -270,6 +290,24 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal AssociationManager AssociationManager { get; private set; } /// <summary> + /// Gets the <see cref="OpenIdRelyingParty"/> instance used to process authentication responses + /// without verifying the assertion or consuming nonces. + /// </summary> + protected OpenIdRelyingParty NonVerifyingRelyingParty { + get { + if (this.nonVerifyingRelyingParty == null) { + lock (this.nonVerifyingRelyingPartyInitLock) { + if (this.nonVerifyingRelyingParty == null) { + this.nonVerifyingRelyingParty = OpenIdRelyingParty.CreateNonVerifying(); + } + } + } + + return this.nonVerifyingRelyingParty; + } + } + + /// <summary> /// Creates an authentication request to verify that a user controls /// some given Identifier. /// </summary> @@ -389,13 +427,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <para>No exception is thrown if no OpenID endpoints were discovered. /// An empty enumerable is returned instead.</para> /// </remarks> - public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + public virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null); Contract.Requires<ArgumentNullException>(realm != null); Contract.Requires<ArgumentNullException>(returnToUrl != null); Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null); - return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>(); + return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>().CacheGeneratedResults(); } /// <summary> @@ -487,6 +525,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> /// </remarks> public IAuthenticationResponse GetResponse() { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); return this.GetResponse(this.Channel.GetRequestFromContext()); } @@ -533,6 +572,52 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <returns>The HTTP response to send to this HTTP request.</returns> + /// <remarks> + /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para> + /// </remarks> + public OutgoingWebResponse ProcessResponseFromPopup() { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + return this.ProcessResponseFromPopup(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param> + /// <returns>The HTTP response to send to this HTTP request.</returns> + public OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + return this.ProcessResponseFromPopup(request, null); + } + + /// <summary> + /// Allows an OpenID extension to read data out of an unverified positive authentication assertion + /// and send it down to the client browser so that Javascript running on the page can perform + /// some preprocessing on the extension data. + /// </summary> + /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam> + /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param> + /// <remarks> + /// This method should be called before <see cref="ProcessResponseFromPopup()"/>. + /// </remarks> + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] + public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName)); + ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); + foreach (var ext in this.clientScriptExtensions.Keys) { + ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); + } + this.clientScriptExtensions.Add(typeof(T), propertyName); + } + #region IDisposable Members /// <summary> @@ -580,6 +665,66 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication. + /// </summary> + /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param> + /// <param name="callback">The callback fired after the response status has been determined but before the Javascript response is formulated.</param> + /// <returns> + /// The HTTP response to send to this HTTP request. + /// </returns> + internal OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request, Action<AuthenticationStatus> callback) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); + + string extensionsJson = null; + var authResponse = this.NonVerifyingRelyingParty.GetResponse(); + ErrorUtilities.VerifyProtocol(authResponse != null, "OpenID popup window or iframe did not recognize an OpenID response in the request."); + + // Give the caller a chance to notify the hosting page and fill up the clientScriptExtensions collection. + if (callback != null) { + callback(authResponse.Status); + } + + Logger.OpenId.DebugFormat("Popup or iframe callback from OP: {0}", request.Url); + Logger.Controls.DebugFormat( + "An authentication response was found in a popup window or iframe using a non-verifying RP with status: {0}", + authResponse.Status); + if (authResponse.Status == AuthenticationStatus.Authenticated) { + var extensionsDictionary = new Dictionary<string, string>(); + foreach (var pair in this.clientScriptExtensions) { + IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); + if (extension == null) { + continue; + } + var positiveResponse = (PositiveAuthenticationResponse)authResponse; + string js = extension.InitializeJavaScriptData(positiveResponse.Response); + if (!string.IsNullOrEmpty(js)) { + extensionsDictionary[pair.Value] = js; + } + } + + extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true); + } + + string payload = "document.URL"; + if (request.HttpMethod == "POST") { + // Promote all form variables to the query string, but since it won't be passed + // to any server (this is a javascript window-to-window transfer) the length of + // it can be arbitrarily long, whereas it was POSTed here probably because it + // was too long for HTTP transit. + UriBuilder payloadUri = new UriBuilder(request.Url); + payloadUri.AppendQueryArgs(request.Form.ToDictionary()); + payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); + } + + if (!string.IsNullOrEmpty(extensionsJson)) { + payload += ", " + extensionsJson; + } + + return InvokeParentPageScript("dnoa_internal.processAuthorizationResult(" + payload + ")"); + } + + /// <summary> /// Performs discovery on the specified identifier. /// </summary> /// <param name="identifier">The identifier to discover services for.</param> @@ -619,8 +764,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Releases unmanaged and - optionally - managed resources /// </summary> /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - private void Dispose(bool disposing) { + protected virtual void Dispose(bool disposing) { if (disposing) { + if (this.nonVerifyingRelyingParty != null) { + this.nonVerifyingRelyingParty.Dispose(); + this.nonVerifyingRelyingParty = null; + } + // Tear off the instance member as a local variable for thread safety. IDisposable disposableChannel = this.channel as IDisposable; if (disposableChannel != null) { @@ -630,6 +780,42 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Invokes a method on a parent frame or window and closes the calling popup window if applicable. + /// </summary> + /// <param name="methodCall">The method to call on the parent window, including + /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> + /// <returns>The entire HTTP response to send to the popup window or iframe to perform the invocation.</returns> + private static OutgoingWebResponse InvokeParentPageScript(string methodCall) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(methodCall)); + + Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); + StringBuilder builder = new StringBuilder(); + builder.AppendLine("<html><body><script type='text/javascript' language='javascript'><!--"); + builder.AppendLine("//<![CDATA["); + builder.Append(@" var inPopup = !window.frameElement; + var objSrc = inPopup ? window.opener : window.frameElement; +"); + + // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable, + // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already + // whether to call window.self.close() after the call. + string htmlFormat = @" if (inPopup) {{ + objSrc.{0}; + window.self.close(); + }} else {{ + objSrc.{0}; + }}"; + builder.AppendFormat(CultureInfo.InvariantCulture, htmlFormat, methodCall); + builder.AppendLine("//]]>--></script>"); + builder.AppendLine("</body></html>"); + + var response = new OutgoingWebResponse(); + response.Body = builder.ToString(); + response.Headers.Add(HttpResponseHeader.ContentType, new ContentType("text/html").ToString()); + return response; + } + + /// <summary> /// Called by derived classes when behaviors are added or removed. /// </summary> /// <param name="sender">The collection being modified.</param> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs index 12d676b..f22645f 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.cs @@ -16,6 +16,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Linq; using System.Text; using System.Web; + using System.Web.Script.Serialization; using System.Web.UI; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -31,30 +32,30 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { internal const string EmbeddedAjaxJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyAjaxControlBase.js"; /// <summary> - /// The name of the javascript function that will initiate a synchronous callback. + /// The "dnoa.op_endpoint" string. /// </summary> - protected const string CallbackJSFunction = "window.dnoa_internal.callback"; + internal const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; /// <summary> - /// The name of the javascript function that will initiate an asynchronous callback. + /// The "dnoa.claimed_id" string. /// </summary> - protected const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; + internal const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; /// <summary> /// The name of the javascript field that stores the maximum time a positive assertion is /// good for before it must be refreshed. /// </summary> - private const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; + internal const string MaxPositiveAssertionLifetimeJsName = "window.dnoa_internal.maxPositiveAssertionLifetime"; /// <summary> - /// The "dnoa.op_endpoint" string. + /// The name of the javascript function that will initiate an asynchronous callback. /// </summary> - private const string OPEndpointParameterName = OpenIdUtilities.CustomParameterPrefix + "op_endpoint"; + protected internal const string CallbackJSFunctionAsync = "window.dnoa_internal.callbackAsync"; /// <summary> - /// The "dnoa.claimed_id" string. + /// The name of the javascript function that will initiate a synchronous callback. /// </summary> - private const string ClaimedIdParameterName = OpenIdUtilities.CustomParameterPrefix + "claimed_id"; + protected const string CallbackJSFunction = "window.dnoa_internal.callback"; #region Property viewstate keys @@ -86,11 +87,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.None; /// <summary> - /// Backing field for the <see cref="RelyingPartyNonVerifying"/> property. - /// </summary> - private static OpenIdRelyingParty relyingPartyNonVerifying; - - /// <summary> /// The authentication response that just came in. /// </summary> private IAuthenticationResponse authenticationResponse; @@ -102,12 +98,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private string discoveryResult; /// <summary> - /// A dictionary of extension response types and the javascript member - /// name to map them to on the user agent. - /// </summary> - private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>(); - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingPartyAjaxControlBase"/> class. /// </summary> protected OpenIdRelyingPartyAjaxControlBase() { @@ -152,6 +142,31 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use. + /// </summary> + /// <value> + /// The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file. + /// </value> + /// <remarks> + /// A performance optimization would be to store off the + /// instance as a static member in your web site and set it + /// to this property in your <see cref="Control.Load">Page.Load</see> + /// event since instantiating these instances can be expensive on + /// heavily trafficked web pages. + /// </remarks> + public override OpenIdRelyingParty RelyingParty { + get { + return base.RelyingParty; + } + + set { + // Make sure we get an AJAX-ready instance. + ErrorUtilities.VerifyArgument(value is OpenIdAjaxRelyingParty, OpenIdStrings.TypeMustImplementX, typeof(OpenIdAjaxRelyingParty).Name); + base.RelyingParty = value; + } + } + + /// <summary> /// Gets the completed authentication response. /// </summary> public IAuthenticationResponse AuthenticationResponse { @@ -193,22 +208,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field). + /// Gets the relying party as its AJAX type. /// </summary> - /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value> - protected abstract string OpenIdAuthDataFormKey { get; } + protected OpenIdAjaxRelyingParty AjaxRelyingParty { + get { return (OpenIdAjaxRelyingParty)this.RelyingParty; } + } /// <summary> - /// Gets the relying party to use when verification of incoming messages is NOT wanted. + /// Gets the name of the open id auth data form key (for the value as stored at the user agent as a FORM field). /// </summary> - private static OpenIdRelyingParty RelyingPartyNonVerifying { - get { - if (relyingPartyNonVerifying == null) { - relyingPartyNonVerifying = OpenIdRelyingParty.CreateNonVerifying(); - } - return relyingPartyNonVerifying; - } - } + /// <value>Usually a concatenation of the control's name and <c>"_openidAuthData"</c>.</value> + protected abstract string OpenIdAuthDataFormKey { get; } /// <summary> /// Gets or sets a value indicating whether an authentication in the page's view state @@ -232,11 +242,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse { Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName)); - ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); - foreach (var ext in this.clientScriptExtensions.Keys) { - ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); - } - this.clientScriptExtensions.Add(typeof(T), propertyName); + this.RelyingParty.RegisterClientScriptExtension<T>(propertyName); } #region ICallbackEventHandler Members @@ -263,27 +269,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion /// <summary> - /// Creates the authentication requests for a given user-supplied Identifier. - /// </summary> - /// <param name="identifier">The identifier to create a request for.</param> - /// <returns> - /// A sequence of authentication requests, any one of which may be - /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>. - /// </returns> - protected internal override IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) { - // If this control is actually a member of another OpenID RP control, - // delegate creation of requests to the parent control. - var parentOwner = this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().FirstOrDefault(); - if (parentOwner != null) { - return parentOwner.CreateRequests(identifier); - } else { - // We delegate all our logic to another method, since invoking base. methods - // within an iterator method results in unverifiable code. - return this.CreateRequestsCore(base.CreateRequests(identifier)); - } - } - - /// <summary> /// Returns the results of a callback event that targets a control. /// </summary> /// <returns>The result of the callback.</returns> @@ -305,7 +290,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { Logger.OpenId.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier); this.Identifier = userSuppliedIdentifier; - this.discoveryResult = this.SerializeDiscoveryAsJson(this.Identifier); + + var serializer = new JavaScriptSerializer(); + IEnumerable<IAuthenticationRequest> requests = this.CreateRequests(this.Identifier); + this.discoveryResult = serializer.Serialize(this.AjaxRelyingParty.AsJsonDiscoveryResult(requests)); + } + + /// <summary> + /// Creates the relying party instance used to generate authentication requests. + /// </summary> + /// <param name="store">The store to pass to the relying party constructor.</param> + /// <returns>The instantiated relying party.</returns> + protected override OpenIdRelyingParty CreateRelyingParty(IRelyingPartyApplicationStore store) { + return new OpenIdAjaxRelyingParty(store); } /// <summary> @@ -323,8 +320,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="identifiers">The identifiers to perform discovery on.</param> protected void PreloadDiscovery(IEnumerable<Identifier> identifiers) { - string discoveryResults = this.SerializeDiscoveryAsJson(identifiers); - string script = "window.dnoa_internal.loadPreloadedDiscoveryResults(" + discoveryResults + ");"; + string script = this.AjaxRelyingParty.AsAjaxPreloadedDiscoveryResult( + identifiers.SelectMany(id => this.CreateRequests(id))); this.Page.ClientScript.RegisterClientScriptBlock(typeof(OpenIdRelyingPartyAjaxControlBase), this.ClientID, script, true); } @@ -402,6 +399,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); base.Render(writer); // Emit a hidden field to let the javascript on the user agent know if an @@ -420,172 +418,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Notifies the user agent via an AJAX response of a completed authentication attempt. /// </summary> protected override void ScriptClosingPopupOrIFrame() { - Logger.OpenId.DebugFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); - string extensionsJson = null; - - var authResponse = RelyingPartyNonVerifying.GetResponse(); - Logger.Controls.DebugFormat( - "The {0} control checked for an authentication response from a popup window or iframe using a non-verifying RP and found: {1}", - this.ID, - authResponse.Status); - if (authResponse.Status == AuthenticationStatus.Authenticated) { - this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. - var extensionsDictionary = new Dictionary<string, string>(); - foreach (var pair in this.clientScriptExtensions) { - IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); - if (extension == null) { - continue; - } - var positiveResponse = (PositiveAuthenticationResponse)authResponse; - string js = extension.InitializeJavaScriptData(positiveResponse.Response); - if (!string.IsNullOrEmpty(js)) { - extensionsDictionary[pair.Value] = js; - } + Action<AuthenticationStatus> callback = status => { + if (status == AuthenticationStatus.Authenticated) { + this.OnUnconfirmedPositiveAssertion(); // event handler will fill the clientScriptExtensions collection. } + }; - extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true); - } - - string payload = "document.URL"; - if (Page.Request.HttpMethod == "POST") { - // Promote all form variables to the query string, but since it won't be passed - // to any server (this is a javascript window-to-window transfer) the length of - // it can be arbitrarily long, whereas it was POSTed here probably because it - // was too long for HTTP transit. - UriBuilder payloadUri = new UriBuilder(Page.Request.Url); - payloadUri.AppendQueryArgs(Page.Request.Form.ToDictionary()); - payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri); - } - - if (!string.IsNullOrEmpty(extensionsJson)) { - payload += ", " + extensionsJson; - } - - this.CallbackUserAgentMethod("dnoa_internal.processAuthorizationResult(" + payload + ")"); - } - - /// <summary> - /// Serializes the discovery of multiple identifiers as a JSON object. - /// </summary> - /// <param name="identifiers">The identifiers to perform discovery on and create requests for.</param> - /// <returns>The serialized JSON object.</returns> - private string SerializeDiscoveryAsJson(IEnumerable<Identifier> identifiers) { - ErrorUtilities.VerifyArgumentNotNull(identifiers, "identifiers"); - - // We prepare a JSON object with this interface: - // Array discoveryWrappers; - // Where each element in the above array has this interface: - // class discoveryWrapper { - // string userSuppliedIdentifier; - // jsonResponse discoveryResult; // contains result of call to SerializeDiscoveryAsJson(Identifier) - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("["); - foreach (var identifier in identifiers) { // TODO: parallelize discovery on these identifiers - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("userSuppliedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(identifier)); - discoveryResultBuilder.AppendFormat("discoveryResult: {0}", this.SerializeDiscoveryAsJson(identifier)); - discoveryResultBuilder.Append("},"); - } - - discoveryResultBuilder.Length -= 1; // trim last comma - discoveryResultBuilder.Append("]"); - return discoveryResultBuilder.ToString(); - } - - /// <summary> - /// Serializes the results of discovery and the created auth requests as a JSON object - /// for the user agent to initiate. - /// </summary> - /// <param name="identifier">The identifier to perform discovery on.</param> - /// <returns>The JSON string.</returns> - private string SerializeDiscoveryAsJson(Identifier identifier) { - ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); - - // We prepare a JSON object with this interface: - // class jsonResponse { - // string claimedIdentifier; - // Array requests; // never null - // string error; // null if no error - // } - // Each element in the requests array looks like this: - // class jsonAuthRequest { - // string endpoint; // URL to the OP endpoint - // string immediate; // URL to initiate an immediate request - // string setup; // URL to initiate a setup request. - // } - StringBuilder discoveryResultBuilder = new StringBuilder(); - discoveryResultBuilder.Append("{"); - try { - IEnumerable<IAuthenticationRequest> requests = this.CreateRequests(identifier).CacheGeneratedResults(); - if (requests.Any()) { - discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", MessagingUtilities.GetSafeJavascriptValue(requests.First().ClaimedIdentifier)); - discoveryResultBuilder.Append("requests: ["); - foreach (IAuthenticationRequest request in requests) { - discoveryResultBuilder.Append("{"); - discoveryResultBuilder.AppendFormat("endpoint: {0},", MessagingUtilities.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Immediate; - OutgoingWebResponse response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("immediate: {0},", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - request.Mode = AuthenticationRequestMode.Setup; - response = request.RedirectingResponse; - discoveryResultBuilder.AppendFormat("setup: {0}", MessagingUtilities.GetSafeJavascriptValue(response.GetDirectUriRequest(this.RelyingParty.Channel).AbsoluteUri)); - discoveryResultBuilder.Append("},"); - } - discoveryResultBuilder.Length -= 1; // trim off last comma - discoveryResultBuilder.Append("]"); - } else { - discoveryResultBuilder.Append("requests: [],"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); - } - } catch (ProtocolException ex) { - discoveryResultBuilder.Append("requests: [],"); - discoveryResultBuilder.AppendFormat("error: {0}", MessagingUtilities.GetSafeJavascriptValue(ex.Message)); - } - - discoveryResultBuilder.Append("}"); - return discoveryResultBuilder.ToString(); - } - - /// <summary> - /// Creates the authentication requests for a given user-supplied Identifier. - /// </summary> - /// <param name="requests">The authentication requests to prepare.</param> - /// <returns> - /// A sequence of authentication requests, any one of which may be - /// used to determine the user's control of the <see cref="IAuthenticationRequest.ClaimedIdentifier"/>. - /// </returns> - private IEnumerable<IAuthenticationRequest> CreateRequestsCore(IEnumerable<IAuthenticationRequest> requests) { - ErrorUtilities.VerifyArgumentNotNull(requests, "requests"); // NO CODE CONTRACTS! (yield return used here) - - // Configure each generated request. - int reqIndex = 0; - foreach (var req in requests) { - req.SetUntrustedCallbackArgument("index", (reqIndex++).ToString(CultureInfo.InvariantCulture)); - - // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter - if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)[AuthenticationRequest.UserSuppliedIdentifierParameterName])) { - Identifier userSuppliedIdentifier = ((AuthenticationRequest)req).DiscoveryResult.UserSuppliedIdentifier; - req.SetUntrustedCallbackArgument(AuthenticationRequest.UserSuppliedIdentifierParameterName, userSuppliedIdentifier.OriginalString); - } - - // Our javascript needs to let the user know which endpoint responded. So we force it here. - // This gives us the info even for 1.0 OPs and 2.0 setup_required responses. - req.SetUntrustedCallbackArgument(OPEndpointParameterName, req.Provider.Uri.AbsoluteUri); - req.SetUntrustedCallbackArgument(ClaimedIdParameterName, (string)req.ClaimedIdentifier ?? string.Empty); - - // Inform ourselves in return_to that we're in a popup or iframe. - req.SetUntrustedCallbackArgument(UIPopupCallbackKey, "1"); - - // We append a # at the end so that if the OP happens to support it, - // the OpenID response "query string" is appended after the hash rather than before, resulting in the - // browser being super-speedy in closing the popup window since it doesn't try to pull a newer version - // of the static resource down from the server merely because of a changed URL. - // http://www.nabble.com/Re:-Defining-how-OpenID-should-behave-with-fragments-in-the-return_to-url-p22694227.html - ////TODO: + OutgoingWebResponse response = this.RelyingParty.ProcessResponseFromPopup( + this.RelyingParty.Channel.GetRequestFromContext(), + callback); - yield return req; - } + response.Send(); } /// <summary> @@ -615,33 +458,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, - /// and closes the calling popup window if applicable. - /// </summary> - /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including - /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param> - private void CallbackUserAgentMethod(string methodCall) { - Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall); - Page.Response.Write(@"<html><body><script language='javascript'> - var inPopup = !window.frameElement; - var objSrc = inPopup ? window.opener : window.frameElement; -"); - - // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable, - // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already - // whether to call window.self.close() after the call. - string htmlFormat = @" if (inPopup) {{ - objSrc.{0}; - window.self.close(); - }} else {{ - objSrc.{0}; - }} -</script></body></html>"; - Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall)); - Page.Response.End(); - } - - /// <summary> /// Sets the window.aspnetapppath variable on the user agent so that cookies can be set with the proper path. /// </summary> private void SetWebAppPathOnUserAgent() { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js index 6faad56..4de5188 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyAjaxControlBase.js @@ -701,7 +701,7 @@ window.dnoa_internal.PositiveAssertion = function(uri) { }; window.dnoa_internal.clone = function(obj) { - if (obj === null || typeof (obj) != 'object') { + if (obj === null || typeof (obj) != 'object' || !isNaN(obj)) { // !isNaN catches Date objects return obj; } @@ -710,6 +710,10 @@ window.dnoa_internal.clone = function(obj) { temp[key] = window.dnoa_internal.clone(obj[key]); } + // Copy over some built-in methods that were not included in the above loop, + // but nevertheless may have been overridden. + temp.toString = window.dnoa_internal.clone(obj.toString); + return temp; }; @@ -732,7 +736,7 @@ window.dnoa_internal.clearExpiredPositiveAssertions = function() { var discoveryResult = window.dnoa_internal.discoveryResults[identifier]; if (typeof (discoveryResult) != 'object') { continue; } // skip functions for (var i = 0; i < discoveryResult.length; i++) { - if (discoveryResult[i].result === window.dnoa_internal.authSuccess) { + if (discoveryResult[i] && discoveryResult[i].result === window.dnoa_internal.authSuccess) { if (new Date() - discoveryResult[i].successReceived > window.dnoa_internal.maxPositiveAssertionLifetime) { // This positive assertion is too old, and may eventually be rejected by DNOA during verification. // Let's clear out the positive assertion so it can be renewed. diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs index a4a60d1..5090ecd 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -92,6 +92,21 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver"; + #region Protected internal callback parameter names + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. + /// </summary> + protected internal const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; + + /// <summary> + /// The parameter name to include in the formulated auth request so that javascript can know whether + /// the OP advertises support for the UI extension. + /// </summary> + protected internal const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + + #endregion + #region Property category constants /// <summary> @@ -111,18 +126,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { #endregion - #region Callback parameter names - - /// <summary> - /// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe. - /// </summary> - protected const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; - - /// <summary> - /// The parameter name to include in the formulated auth request so that javascript can know whether - /// the OP advertises support for the UI extension. - /// </summary> - protected const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported"; + #region Private callback parameter names /// <summary> /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. @@ -293,10 +297,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// heavily trafficked web pages. /// </remarks> [Browsable(false)] - public OpenIdRelyingParty RelyingParty { + public virtual OpenIdRelyingParty RelyingParty { get { if (this.relyingParty == null) { this.relyingParty = this.CreateRelyingParty(); + this.ConfigureRelyingParty(this.relyingParty); this.relyingPartyOwned = true; } return this.relyingParty; @@ -353,6 +358,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } set { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(value)); + if (Page != null && !DesignMode) { // Validate new value by trying to construct a Realm object based on it. new Realm(OpenIdUtilities.GetResolvedRealm(this.Page, value, this.RelyingParty.Channel.GetRequestFromContext())); // throws an exception on failure. @@ -556,8 +563,15 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { protected internal virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier identifier) { Contract.Requires<ArgumentNullException>(identifier != null); - // Delegate to a private method to keep 'yield return' and Code Contract separate. - return this.CreateRequestsCore(identifier); + // If this control is actually a member of another OpenID RP control, + // delegate creation of requests to the parent control. + var parentOwner = this.ParentControls.OfType<OpenIdRelyingPartyControlBase>().FirstOrDefault(); + if (parentOwner != null) { + return parentOwner.CreateRequests(identifier); + } else { + // Delegate to a private method to keep 'yield return' and Code Contract separate. + return this.CreateRequestsCore(identifier); + } } /// <summary> @@ -634,6 +648,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Notifies the user agent via an AJAX response of a completed authentication attempt. + /// </summary> + protected virtual void ScriptClosingPopupOrIFrame() { + this.RelyingParty.ProcessResponseFromPopup(); + } + + /// <summary> /// Called when the <see cref="Identifier"/> property is changed. /// </summary> protected virtual void OnIdentifierChanged() { @@ -769,29 +790,32 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Creates the relying party instance used to generate authentication requests. /// </summary> /// <returns>The instantiated relying party.</returns> - protected virtual OpenIdRelyingParty CreateRelyingParty() { - return this.CreateRelyingParty(true); + protected OpenIdRelyingParty CreateRelyingParty() { + IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); + return this.CreateRelyingParty(store); } /// <summary> /// Creates the relying party instance used to generate authentication requests. /// </summary> - /// <param name="verifySignature"> - /// A value indicating whether message protections should be applied to the processed messages. - /// Use <c>false</c> to postpone verification to a later time without invalidating nonces. - /// </param> + /// <param name="store">The store to pass to the relying party constructor.</param> /// <returns>The instantiated relying party.</returns> - protected virtual OpenIdRelyingParty CreateRelyingParty(bool verifySignature) { - IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); - var rp = verifySignature ? new OpenIdRelyingParty(store) : OpenIdRelyingParty.CreateNonVerifying(); + protected virtual OpenIdRelyingParty CreateRelyingParty(IRelyingPartyApplicationStore store) { + return new OpenIdRelyingParty(store); + } + + /// <summary> + /// Configures the relying party. + /// </summary> + /// <param name="relyingParty">The relying party.</param> + protected virtual void ConfigureRelyingParty(OpenIdRelyingParty relyingParty) { + Contract.Requires<ArgumentNullException>(relyingParty != null); // Only set RequireSsl to true, as we don't want to override // a .config setting of true with false. if (this.RequireSsl) { - rp.SecuritySettings.RequireSsl = true; + relyingParty.SecuritySettings.RequireSsl = true; } - - return rp; } /// <summary> @@ -841,21 +865,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> - /// Wires the popup window to close itself and pass the authentication result to the parent window. - /// </summary> - protected virtual void ScriptClosingPopupOrIFrame() { - StringBuilder startupScript = new StringBuilder(); - startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL);"); - startupScript.AppendLine("window.close();"); - - this.Page.ClientScript.RegisterStartupScript(typeof(OpenIdRelyingPartyControlBase), "loginPopupClose", startupScript.ToString(), true); - - // TODO: alternately we should probably take over rendering this page here to avoid - // a lot of unnecessary work on the server and possible momentary display of the - // page in the popup window. - } - - /// <summary> /// Creates the identifier-persisting cookie, either for saving or deleting. /// </summary> /// <param name="response">The positive authentication response; or <c>null</c> to clear the cookie.</param> @@ -939,7 +948,11 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { if (req.DiscoveryResult.IsExtensionSupported<UIRequest>()) { // Inform the OP that we'll be using a popup window consistent with the UI extension. - req.AddExtension(new UIRequest()); + // But beware that the extension MAY have already been added if we're using + // the OpenIdAjaxRelyingParty class. + if (!((AuthenticationRequest)req).Extensions.OfType<UIRequest>().Any()) { + req.AddExtension(new UIRequest()); + } // Provide a hint for the client javascript about whether the OP supports the UI extension. // This is so the window can be made the correct size for the extension. @@ -1029,67 +1042,5 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return false; } - - /// <summary> - /// An authentication request comparer that judges equality solely on the OP endpoint hostname. - /// </summary> - private class DuplicateRequestedHostsComparer : IEqualityComparer<IAuthenticationRequest> { - /// <summary> - /// The singleton instance of this comparer. - /// </summary> - private static IEqualityComparer<IAuthenticationRequest> instance = new DuplicateRequestedHostsComparer(); - - /// <summary> - /// Prevents a default instance of the <see cref="DuplicateRequestedHostsComparer"/> class from being created. - /// </summary> - private DuplicateRequestedHostsComparer() { - } - - /// <summary> - /// Gets the singleton instance of this comparer. - /// </summary> - internal static IEqualityComparer<IAuthenticationRequest> Instance { - get { return instance; } - } - - #region IEqualityComparer<IAuthenticationRequest> Members - - /// <summary> - /// Determines whether the specified objects are equal. - /// </summary> - /// <param name="x">The first object to compare.</param> - /// <param name="y">The second object to compare.</param> - /// <returns> - /// true if the specified objects are equal; otherwise, false. - /// </returns> - public bool Equals(IAuthenticationRequest x, IAuthenticationRequest y) { - if (x == null && y == null) { - return true; - } - - if (x == null || y == null) { - return false; - } - - // We'll distinguish based on the host name only, which - // admittedly is only a heuristic, but if we remove one that really wasn't a duplicate, well, - // this multiple OP attempt thing was just a convenience feature anyway. - return string.Equals(x.Provider.Uri.Host, y.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Returns a hash code for the specified object. - /// </summary> - /// <param name="obj">The <see cref="T:System.Object"/> for which a hash code is to be returned.</param> - /// <returns>A hash code for the specified object.</returns> - /// <exception cref="T:System.ArgumentNullException"> - /// The type of <paramref name="obj"/> is a reference type and <paramref name="obj"/> is null. - /// </exception> - public int GetHashCode(IAuthenticationRequest obj) { - return obj.Provider.Uri.Host.GetHashCode(); - } - - #endregion - } } } diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs index e93383d..b7a54eb 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdSelector.cs @@ -81,11 +81,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { private HiddenField positiveAssertionField; /// <summary> - /// A field to store the value to set on the <see cref="textBox"/> control after it's created. - /// </summary> - private bool downloadYuiLibrary = OpenIdAjaxTextBox.DownloadYahooUILibraryDefault; - - /// <summary> /// Initializes a new instance of the <see cref="OpenIdSelector"/> class. /// </summary> public OpenIdSelector() { @@ -102,6 +97,50 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public event EventHandler<TokenProcessingErrorEventArgs> TokenProcessingError; /// <summary> + /// Gets the text box where applicable. + /// </summary> + public OpenIdAjaxTextBox TextBox { + get { + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox; + } + } + + /// <summary> + /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with. + /// </summary> + [Browsable(true), DefaultValue(OpenIdAjaxTextBox.ThrottleDefault), Category(BehaviorCategory)] + [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")] + public int Throttle { + get { + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.Throttle; + } + + set { + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.Throttle = value; + } + } + + /// <summary> + /// Gets or sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user. + /// </summary> + [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:08"), Category(BehaviorCategory)] + [Description("The time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.")] + public TimeSpan Timeout { + get { + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.Timeout; + } + + set { + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.Timeout = value; + } + } + + /// <summary> /// Gets or sets the tool tip text that appears on the green checkmark when authentication succeeds. /// </summary> [Bindable(true), DefaultValue(AuthenticatedAsToolTipDefault), Localizable(true), Category(AppearanceCategory)] @@ -126,19 +165,13 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { [Description("Whether a split button will be used for the \"log in\" when the user provides an identifier that delegates to more than one Provider.")] public bool DownloadYahooUILibrary { get { - return this.textBox != null ? this.textBox.DownloadYahooUILibrary : this.downloadYuiLibrary; + this.EnsureChildControlsAreCreatedSafe(); + return this.textBox.DownloadYahooUILibrary; } set { - // We don't just call EnsureChildControls() and then set the property on - // this.textBox itself because (apparently) setting this property in the ASPX - // page and thus calling this EnsureID() via EnsureChildControls() this early - // results in no ID. - if (this.textBox != null) { - this.textBox.DownloadYahooUILibrary = value; - } else { - this.downloadYuiLibrary = value; - } + this.EnsureChildControlsAreCreatedSafe(); + this.textBox.DownloadYahooUILibrary = value; } } @@ -207,10 +240,37 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering. /// </summary> protected override void CreateChildControls() { + this.EnsureChildControlsAreCreatedSafe(); + base.CreateChildControls(); + + // Now do the ID specific work. this.EnsureID(); ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(this.UniqueID), "Control.EnsureID() failed to give us a unique ID. Try setting an ID on the OpenIdSelector control. But please also file this bug with the project owners."); + this.Controls.Add(this.textBox); + + this.positiveAssertionField.ID = this.ID + AuthDataFormKeySuffix; + this.Controls.Add(this.positiveAssertionField); + } + + /// <summary> + /// Ensures that the child controls have been built, but doesn't set control + /// properties that require executing <see cref="Control.EnsureID"/> in order to avoid + /// certain initialization order problems. + /// </summary> + /// <remarks> + /// We don't just call EnsureChildControls() and then set the property on + /// this.textBox itself because (apparently) setting this property in the ASPX + /// page and thus calling this EnsureID() via EnsureChildControls() this early + /// results in no ID. + /// </remarks> + protected virtual void EnsureChildControlsAreCreatedSafe() { + // If we've already created the child controls, this method is a no-op. + if (this.textBox != null) { + return; + } + var selectorButton = this.Buttons.OfType<SelectorInfoCardButton>().FirstOrDefault(); if (selectorButton != null) { var selector = selectorButton.InfoCardSelector; @@ -225,12 +285,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.textBox.ID = "openid_identifier"; this.textBox.HookFormSubmit = false; this.textBox.ShowLogOnPostBackButton = true; - this.textBox.DownloadYahooUILibrary = this.downloadYuiLibrary; - this.Controls.Add(this.textBox); this.positiveAssertionField = new HiddenField(); - this.positiveAssertionField.ID = this.ID + AuthDataFormKeySuffix; - this.Controls.Add(this.positiveAssertionField); } /// <summary> @@ -254,11 +310,16 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.EnsureValidButtons(); var css = new HtmlLink(); - css.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedStylesheetResourceName); - css.Attributes["rel"] = "stylesheet"; - css.Attributes["type"] = "text/css"; - ErrorUtilities.VerifyHost(this.Page.Header != null, OpenIdStrings.HeadTagMustIncludeRunatServer); - this.Page.Header.Controls.AddAt(0, css); // insert at top so host page can override + try { + css.Href = this.Page.ClientScript.GetWebResourceUrl(this.GetType(), EmbeddedStylesheetResourceName); + css.Attributes["rel"] = "stylesheet"; + css.Attributes["type"] = "text/css"; + ErrorUtilities.VerifyHost(this.Page.Header != null, OpenIdStrings.HeadTagMustIncludeRunatServer); + this.Page.Header.Controls.AddAt(0, css); // insert at top so host page can override + } catch { + css.Dispose(); + throw; + } // Import the .js file where most of the code is. this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdSelector), EmbeddedScriptResourceName); @@ -288,6 +349,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract"); writer.AddAttribute(HtmlTextWriterAttribute.Class, "OpenIdProviders"); writer.RenderBeginTag(HtmlTextWriterTag.Ul); diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs index 4d635fb..335b435 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -4,7 +4,7 @@ // </copyright> //----------------------------------------------------------------------- -[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox.EmbeddedLogoResourceName, "image/gif")] +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox.EmbeddedLogoResourceName, "image/png")] #pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use @@ -45,7 +45,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// The name of the manifest stream containing the /// OpenID logo that is placed inside the text box. /// </summary> - internal const string EmbeddedLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.openid_login.gif"; + internal const string EmbeddedLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.openid_login.png"; /// <summary> /// Default value for <see cref="TabIndex"/> property. @@ -584,6 +584,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); + if (this.ShowLogo) { string logoUrl = Page.ClientScript.GetWebResourceUrl( typeof(OpenIdTextBox), EmbeddedLogoResourceName); @@ -625,6 +627,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// true if the server control's state changes as a result of the postback; otherwise, false. /// </returns> protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) { + Contract.Assume(postCollection != null, "Missing contract"); + // If the control was temporarily hidden, it won't be in the Form data, // and we'll just implicitly keep the last Text setting. if (postCollection[this.Name] != null) { diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index b6a1b76..3e2298c 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -146,6 +146,14 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } } + // Check whether this particular identifier presents a problem with HTTP discovery + // due to limitations in the .NET Uri class. + UriIdentifier claimedIdUri = claimedId as UriIdentifier; + if (claimedIdUri != null && claimedIdUri.ProblematicNormalization) { + ErrorUtilities.VerifyProtocol(relyingParty.SecuritySettings.AllowApproximateIdentifierDiscovery, OpenIdStrings.ClaimedIdentifierDefiesDotNetNormalization); + Logger.OpenId.WarnFormat("Positive assertion for claimed identifier {0} cannot be precisely verified under partial trust hosting due to .NET limitation. An approximate verification will be attempted.", claimedId); + } + // While it LOOKS like we're performing discovery over HTTP again // Yadis.IdentifierDiscoveryCachePolicy is set to HttpRequestCacheLevel.CacheIfAvailable // which means that the .NET runtime is caching our discoveries for us. This turns out diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs index 071a488..a7686c5 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/RelyingPartySecuritySettings.cs @@ -16,11 +16,18 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> public sealed class RelyingPartySecuritySettings : SecuritySettings { /// <summary> + /// The default value for the <see cref="ProtectDownlevelReplayAttacks"/> property. + /// </summary> + internal const bool ProtectDownlevelReplayAttacksDefault = true; + + /// <summary> /// Initializes a new instance of the <see cref="RelyingPartySecuritySettings"/> class. /// </summary> internal RelyingPartySecuritySettings() : base(false) { this.PrivateSecretMaximumAge = TimeSpan.FromDays(7); + this.ProtectDownlevelReplayAttacks = ProtectDownlevelReplayAttacksDefault; + this.AllowApproximateIdentifierDiscovery = true; } /// <summary> @@ -126,6 +133,31 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { public bool AllowDualPurposeIdentifiers { get; set; } /// <summary> + /// Gets or sets a value indicating whether certain Claimed Identifiers that exploit + /// features that .NET does not have the ability to send exact HTTP requests for will + /// still be allowed by using an approximate HTTP request. + /// </summary> + /// <value> + /// The default value is <c>true</c>. + /// </value> + public bool AllowApproximateIdentifierDiscovery { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether special measures are taken to + /// protect users from replay attacks when those users' identities are hosted + /// by OpenID 1.x Providers. + /// </summary> + /// <value>The default value is <c>true</c>.</value> + /// <remarks> + /// <para>Nonces for protection against replay attacks were not mandated + /// by OpenID 1.x, which leaves users open to replay attacks.</para> + /// <para>This feature works by adding a signed nonce to the authentication request. + /// This might increase the request size beyond what some OpenID 1.1 Providers + /// (such as Blogger) are capable of handling.</para> + /// </remarks> + internal bool ProtectDownlevelReplayAttacks { get; set; } + + /// <summary> /// Filters out any disallowed endpoints. /// </summary> /// <param name="endpoints">The endpoints discovered on an Identifier.</param> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs index 15b6ca7..ac4dcbf 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorOpenIdButton.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Drawing.Design; @@ -24,6 +25,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Initializes a new instance of the <see cref="SelectorOpenIdButton"/> class. + /// </summary> + /// <param name="imageUrl">The image to display on the button.</param> + public SelectorOpenIdButton(string imageUrl) + : this() { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + + this.Image = imageUrl; + } + + /// <summary> /// Gets or sets the path to the image to display on the button's surface. /// </summary> /// <value>The virtual path to the image.</value> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs index 3a05287..2195e73 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/SelectorProviderButton.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; using System.ComponentModel; using System.Diagnostics.Contracts; using System.Drawing.Design; @@ -25,6 +26,20 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { } /// <summary> + /// Initializes a new instance of the <see cref="SelectorProviderButton"/> class. + /// </summary> + /// <param name="providerIdentifier">The OP Identifier.</param> + /// <param name="imageUrl">The image to display on the button.</param> + public SelectorProviderButton(Identifier providerIdentifier, string imageUrl) + : this() { + Contract.Requires<ArgumentNullException>(providerIdentifier != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(imageUrl)); + + this.OPIdentifier = providerIdentifier; + this.Image = imageUrl; + } + + /// <summary> /// Gets or sets the path to the image to display on the button's surface. /// </summary> /// <value>The virtual path to the image.</value> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs index f0a1574..fdb6931 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs @@ -84,19 +84,6 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { return this.associationStore.RemoveAssociation(distinguishingFactor, handle); } - /// <summary> - /// Clears all expired associations from the store. - /// </summary> - /// <remarks> - /// If another algorithm is in place to periodically clear out expired associations, - /// this method call may be ignored. - /// This should be done frequently enough to avoid a memory leak, but sparingly enough - /// to not be a performance drain. - /// </remarks> - public void ClearExpiredAssociations() { - this.associationStore.ClearExpiredAssociations(); - } - #endregion #region INonceStore Members diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.gif b/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.gif Binary files differdeleted file mode 100644 index cde836c..0000000 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.gif +++ /dev/null diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.png b/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.png Binary files differnew file mode 100644 index 0000000..caebd58 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/openid_login.png diff --git a/src/DotNetOpenAuth/OpenId/UriDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/UriDiscoveryService.cs index f017d9f..7d17fd9 100644 --- a/src/DotNetOpenAuth/OpenId/UriDiscoveryService.cs +++ b/src/DotNetOpenAuth/OpenId/UriDiscoveryService.cs @@ -67,6 +67,7 @@ namespace DotNetOpenAuth.OpenId { // Failing YADIS discovery of an XRDS document, we try HTML discovery. if (endpoints.Count == 0) { + yadisResult.TryRevertToHtmlResponse(); var htmlEndpoints = new List<IdentifierDiscoveryResult>(DiscoverFromHtml(yadisResult.NormalizedUri, uriIdentifier, yadisResult.ResponseText)); if (htmlEndpoints.Any()) { Logger.Yadis.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count); diff --git a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs index 0b7a0e3..278ae37 100644 --- a/src/DotNetOpenAuth/OpenId/UriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/UriIdentifier.cs @@ -10,6 +10,9 @@ namespace DotNetOpenAuth.OpenId { using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; + using System.Reflection; + using System.Security; + using System.Text; using System.Text.RegularExpressions; using System.Web.UI.HtmlControls; using System.Xml; @@ -30,6 +33,68 @@ namespace DotNetOpenAuth.OpenId { private static readonly string[] allowedSchemes = { "http", "https" }; /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser roundTrippingHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// The special scheme to use for HTTP URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpParser = new NonPathCompressingUriParser(Uri.UriSchemeHttp); + + /// <summary> + /// The special scheme to use for HTTPS URLs that should not have their paths compressed. + /// </summary> + private static NonPathCompressingUriParser publishableHttpsParser = new NonPathCompressingUriParser(Uri.UriSchemeHttps); + + /// <summary> + /// A value indicating whether scheme substitution is being used to workaround + /// .NET path compression that invalidates some OpenIDs that have trailing periods + /// in one of their path segments. + /// </summary> + private static bool schemeSubstitution; + + /// <summary> + /// Initializes static members of the <see cref="UriIdentifier"/> class. + /// </summary> + /// <remarks> + /// This method attempts to workaround the .NET Uri class parsing bug described here: + /// https://connect.microsoft.com/VisualStudio/feedback/details/386695/system-uri-incorrectly-strips-trailing-dots?wa=wsignin1.0#tabs + /// since some identifiers (like some of the pseudonymous identifiers from Yahoo) include path segments + /// that end with periods, which the Uri class will typically trim off. + /// </remarks> + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Some things just can't be done in a field initializer.")] + static UriIdentifier() { + // Our first attempt to handle trailing periods in path segments is to leverage + // full trust if it's available to rewrite the rules. + // In fact this is the ONLY way in .NET 3.5 (and arguably in .NET 4.0) to send + // outbound HTTP requests with trailing periods, so it's the only way to perform + // discovery on such an identifier. + try { + UriParser.Register(roundTrippingHttpParser, "dnoarthttp", 80); + UriParser.Register(roundTrippingHttpsParser, "dnoarthttps", 443); + UriParser.Register(publishableHttpParser, "dnoahttp", 80); + UriParser.Register(publishableHttpsParser, "dnoahttps", 443); + roundTrippingHttpParser.Initialize(false); + roundTrippingHttpsParser.Initialize(false); + publishableHttpParser.Initialize(true); + publishableHttpsParser.Initialize(true); + schemeSubstitution = true; + Logger.OpenId.Debug(".NET Uri class path compression overridden."); + Reporting.RecordFeatureUse("FullTrust"); + } catch (SecurityException) { + // We must be running in partial trust. Nothing more we can do. + Logger.OpenId.Warn("Unable to coerce .NET to stop compressing URI paths due to partial trust limitations. Some URL identifiers may be unable to complete login."); + Reporting.RecordFeatureUse("PartialTrust"); + } + } + + /// <summary> /// Initializes a new instance of the <see cref="UriIdentifier"/> class. /// </summary> /// <param name="uri">The value this identifier will represent.</param> @@ -62,7 +127,8 @@ namespace DotNetOpenAuth.OpenId { /// Initializes a new instance of the <see cref="UriIdentifier"/> class. /// </summary> /// <param name="uri">The value this identifier will represent.</param> - internal UriIdentifier(Uri uri) : this(uri, false) { + internal UriIdentifier(Uri uri) + : this(uri, false) { } /// <summary> @@ -73,7 +139,13 @@ namespace DotNetOpenAuth.OpenId { internal UriIdentifier(Uri uri, bool requireSslDiscovery) : base(uri != null ? uri.OriginalString : null, requireSslDiscovery) { Contract.Requires<ArgumentNullException>(uri != null); - if (!TryCanonicalize(new UriBuilder(uri), out uri)) { + + string uriAsString = uri.OriginalString; + if (schemeSubstitution) { + uriAsString = NormalSchemeToSpecialRoundTrippingScheme(uriAsString); + } + + if (!TryCanonicalize(uriAsString, out uri)) { throw new UriFormatException(); } if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) { @@ -96,6 +168,26 @@ namespace DotNetOpenAuth.OpenId { internal bool SchemeImplicitlyPrepended { get; private set; } /// <summary> + /// Gets a value indicating whether this Identifier has characters or patterns that + /// the <see cref="Uri"/> class normalizes away and invalidating the Identifier. + /// </summary> + internal bool ProblematicNormalization { + get { + if (schemeSubstitution) { + // With full trust, we have no problematic URIs + return false; + } + + var simpleUri = new SimpleUri(this.OriginalString); + if (simpleUri.Path.EndsWith(".", StringComparison.Ordinal) || simpleUri.Path.Contains("./")) { + return true; + } + + return false; + } + } + + /// <summary> /// Converts a <see cref="UriIdentifier"/> instance to a <see cref="Uri"/> instance. /// </summary> /// <param name="identifier">The identifier to convert to an ordinary <see cref="Uri"/> instance.</param> @@ -137,7 +229,12 @@ namespace DotNetOpenAuth.OpenId { if (other == null) { return false; } - return this.Uri == other.Uri; + + if (this.ProblematicNormalization || other.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).Equals(new SimpleUri(other.OriginalString)); + } else { + return this.Uri == other.Uri; + } } /// <summary> @@ -157,16 +254,13 @@ namespace DotNetOpenAuth.OpenId { /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>. /// </returns> public override string ToString() { - return Uri.AbsoluteUri; - } -#if UNUSED - static bool TryCanonicalize(string uri, out string canonicalUri) { - Uri normalizedUri; - bool result = TryCanonicalize(uri, out normalizedUri); - canonicalUri = normalizedUri.AbsoluteUri; - return result; + if (this.ProblematicNormalization) { + return new SimpleUri(this.OriginalString).ToString(); + } else { + return this.Uri.AbsoluteUri; + } } -#endif + /// <summary> /// Determines whether a URI is a valid OpenID Identifier (of any kind). /// </summary> @@ -222,9 +316,7 @@ namespace DotNetOpenAuth.OpenId { } // Strip the fragment. - UriBuilder builder = new UriBuilder(Uri); - builder.Fragment = null; - return builder.Uri; + return new UriIdentifier(this.OriginalString.Substring(0, this.OriginalString.IndexOf('#'))); } /// <summary> @@ -335,8 +427,12 @@ namespace DotNetOpenAuth.OpenId { schemePrepended = true; } + if (schemeSubstitution) { + uri = NormalSchemeToSpecialRoundTrippingScheme(uri); + } + // Use a UriBuilder because it helps to normalize the URL as well. - return TryCanonicalize(new UriBuilder(uri), out canonicalUri); + return TryCanonicalize(uri, out canonicalUri); } catch (UriFormatException) { // We try not to land here with checks in the try block, but just in case. return false; @@ -344,9 +440,9 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> - /// Removes the fragment from a URL and sets the host to lowercase. + /// Fixes up the scheme if appropriate. /// </summary> - /// <param name="uriBuilder">The URI builder with the value to canonicalize.</param> + /// <param name="uri">The URI to canonicalize.</param> /// <param name="canonicalUri">The resulting canonical URI.</param> /// <returns><c>true</c> if the canonicalization was successful; <c>false</c> otherwise.</returns> /// <remarks> @@ -356,12 +452,48 @@ namespace DotNetOpenAuth.OpenId { /// For this, you should lookup the value stored in IAuthenticationResponse.ClaimedIdentifier. /// </remarks> [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "The user will see the result of this operation and they want to see it in lower case.")] - private static bool TryCanonicalize(UriBuilder uriBuilder, out Uri canonicalUri) { - uriBuilder.Host = uriBuilder.Host.ToLowerInvariant(); - canonicalUri = uriBuilder.Uri; + private static bool TryCanonicalize(string uri, out Uri canonicalUri) { + Contract.Requires<ArgumentNullException>(uri != null); + + if (schemeSubstitution) { + UriBuilder uriBuilder = new UriBuilder(uri); + + // Swap out our round-trippable scheme for the publishable (hidden) scheme. + uriBuilder.Scheme = uriBuilder.Scheme == roundTrippingHttpParser.RegisteredScheme ? publishableHttpParser.RegisteredScheme : publishableHttpsParser.RegisteredScheme; + canonicalUri = uriBuilder.Uri; + } else { + canonicalUri = new Uri(uri); + } + return true; } + /// <summary> + /// Gets the special non-compressing scheme or URL for a standard scheme or URL. + /// </summary> + /// <param name="normal">The ordinary URL or scheme name.</param> + /// <returns>The non-compressing equivalent scheme or URL for the given value.</returns> + private static string NormalSchemeToSpecialRoundTrippingScheme(string normal) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(normal)); + Contract.Requires<InternalErrorException>(schemeSubstitution); + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + + int delimiterIndex = normal.IndexOf(Uri.SchemeDelimiter); + string normalScheme = delimiterIndex < 0 ? normal : normal.Substring(0, delimiterIndex); + string nonCompressingScheme; + if (string.Equals(normalScheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpParser.RegisteredScheme; + } else if (string.Equals(normalScheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase) || + string.Equals(normalScheme, publishableHttpsParser.RegisteredScheme, StringComparison.OrdinalIgnoreCase)) { + nonCompressingScheme = roundTrippingHttpsParser.RegisteredScheme; + } else { + throw new NotSupportedException(); + } + + return delimiterIndex < 0 ? nonCompressingScheme : nonCompressingScheme + normal.Substring(delimiterIndex); + } + #if CONTRACTS_FULL /// <summary> /// Verifies conditions that should be true for any valid state of this object. @@ -374,5 +506,178 @@ namespace DotNetOpenAuth.OpenId { Contract.Invariant(this.Uri.AbsoluteUri != null); } #endif + + /// <summary> + /// A simple URI class that doesn't suffer from the parsing problems of the <see cref="Uri"/> class. + /// </summary> + internal class SimpleUri { + /// <summary> + /// URI characters that separate the URI Path from subsequent elements. + /// </summary> + private static readonly char[] PathEndingCharacters = new char[] { '?', '#' }; + + /// <summary> + /// Initializes a new instance of the <see cref="SimpleUri"/> class. + /// </summary> + /// <param name="value">The value.</param> + internal SimpleUri(string value) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(value)); + + // Leverage the Uri class's parsing where we can. + Uri uri = new Uri(value); + this.Scheme = uri.Scheme; + this.Authority = uri.Authority; + this.Query = uri.Query; + this.Fragment = uri.Fragment; + + // Get the Path out ourselves, since the default Uri parser compresses it too much for OpenID. + int schemeLength = value.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + Contract.Assume(schemeLength > 0); + int hostStart = schemeLength + Uri.SchemeDelimiter.Length; + int hostFinish = value.IndexOf('/', hostStart); + if (hostFinish < 0) { + this.Path = "/"; + } else { + int pathFinish = value.IndexOfAny(PathEndingCharacters, hostFinish); + Contract.Assume(pathFinish >= hostFinish || pathFinish < 0); + if (pathFinish < 0) { + this.Path = value.Substring(hostFinish); + } else { + this.Path = value.Substring(hostFinish, pathFinish - hostFinish); + } + } + + this.Path = NormalizePathEscaping(this.Path); + } + + /// <summary> + /// Gets the scheme. + /// </summary> + /// <value>The scheme.</value> + public string Scheme { get; private set; } + + /// <summary> + /// Gets the authority. + /// </summary> + /// <value>The authority.</value> + public string Authority { get; private set; } + + /// <summary> + /// Gets the path of the URI. + /// </summary> + /// <value>The path from the URI.</value> + public string Path { get; private set; } + + /// <summary> + /// Gets the query. + /// </summary> + /// <value>The query.</value> + public string Query { get; private set; } + + /// <summary> + /// Gets the fragment. + /// </summary> + /// <value>The fragment.</value> + public string Fragment { get; private set; } + + /// <summary> + /// Returns a <see cref="System.String"/> that represents this instance. + /// </summary> + /// <returns> + /// A <see cref="System.String"/> that represents this instance. + /// </returns> + public override string ToString() { + return this.Scheme + Uri.SchemeDelimiter + this.Authority + this.Path + this.Query + this.Fragment; + } + + /// <summary> + /// Determines whether the specified <see cref="System.Object"/> is equal to this instance. + /// </summary> + /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param> + /// <returns> + /// <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + SimpleUri other = obj as SimpleUri; + if (other == null) { + return false; + } + + // Note that this equality check is intentionally leaving off the Fragment part + // to match Uri behavior, and is intentionally being case sensitive and insensitive + // for different parts. + return string.Equals(this.Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Authority, other.Authority, StringComparison.OrdinalIgnoreCase) && + string.Equals(this.Path, other.Path, StringComparison.Ordinal) && + string.Equals(this.Query, other.Query, StringComparison.Ordinal); + } + + /// <summary> + /// Normalizes the characters that are escaped in the given URI path. + /// </summary> + /// <param name="path">The path to normalize.</param> + /// <returns>The given path, with exactly those characters escaped which should be.</returns> + private static string NormalizePathEscaping(string path) { + Contract.Requires<ArgumentNullException>(path != null); + + string[] segments = path.Split('/'); + for (int i = 0; i < segments.Length; i++) { + segments[i] = Uri.EscapeDataString(Uri.UnescapeDataString(segments[i])); + } + + return string.Join("/", segments); + } + } + + /// <summary> + /// A URI parser that does not compress paths, such as trimming trailing periods from path segments. + /// </summary> + private class NonPathCompressingUriParser : GenericUriParser { + /// <summary> + /// The field that stores the scheme that this parser is registered under. + /// </summary> + private static FieldInfo schemeField; + + /// <summary> + /// The standard "http" or "https" scheme that this parser is subverting. + /// </summary> + private string standardScheme; + + /// <summary> + /// Initializes a new instance of the <see cref="NonPathCompressingUriParser"/> class. + /// </summary> + /// <param name="standardScheme">The standard scheme that this parser will be subverting.</param> + public NonPathCompressingUriParser(string standardScheme) + : base(GenericUriParserOptions.DontCompressPath | GenericUriParserOptions.IriParsing | GenericUriParserOptions.Idn) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(standardScheme)); + this.standardScheme = standardScheme; + } + + /// <summary> + /// Gets the scheme this parser is registered under. + /// </summary> + /// <value>The registered scheme.</value> + internal string RegisteredScheme { get; private set; } + + /// <summary> + /// Initializes this parser with the actual scheme it should appear to be. + /// </summary> + /// <param name="hideNonStandardScheme">if set to <c>true</c> Uris using this scheme will look like they're using the original standard scheme.</param> + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Schemes are traditionally displayed in lowercase.")] + internal void Initialize(bool hideNonStandardScheme) { + if (schemeField == null) { + schemeField = typeof(UriParser).GetField("m_Scheme", BindingFlags.NonPublic | BindingFlags.Instance); + } + + this.RegisteredScheme = (string)schemeField.GetValue(this); + + if (hideNonStandardScheme) { + schemeField.SetValue(this, this.standardScheme.ToLowerInvariant()); + } + } + } } } diff --git a/src/DotNetOpenAuth/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth/Properties/AssemblyInfo.cs index ce08780..a63c71e 100644 --- a/src/DotNetOpenAuth/Properties/AssemblyInfo.cs +++ b/src/DotNetOpenAuth/Properties/AssemblyInfo.cs @@ -4,18 +4,6 @@ // </copyright> //----------------------------------------------------------------------- -// Uncomment this line to build a partially trusted assembly. -// This has some security bonuses in that if there was a way to -// hijack this assembly to do something it is not designed to do, -// it will fail before doing much damage. -// But a partially trusted assembly's events, handled by the hosting -// web site, will also be under the partial trust restriction. -// Also note that http://support.microsoft.com/kb/839300 states a -// strong-name signed assembly must use AllowPartiallyTrustedCallers -// to be called from a web page, but defining PARTIAL_TRUST below also -// accomplishes this. -////#define PARTIAL_TRUST - // We DON'T put an AssemblyVersionAttribute in here because it is generated in the build. using System; @@ -69,38 +57,3 @@ using System.Web.UI; #else [assembly: InternalsVisibleTo("DotNetOpenAuth.Test")] #endif - -#if !CLR4 - -// Specify what permissions are required and optional for the assembly. -// In order for CAS to remove unnecessary privileges from this assembly (which is desirable -// for security), we need at least one RequestMinimum and at least one RequestOptional. -// These permissions were determined using PermCalc.exe - -// We need to be allowed to execute code. Besides, it gives a good baseline RequestMinimum permission. -[assembly: SecurityPermission(SecurityAction.RequestMinimum, Execution = true)] - -// Allows the consumer to call out to the web server. This is unnecessary in provider-only scenarios. -// Note: we don't use a single demand for https?://.* because the regex pattern must exactly -// match the one used by hosting providers. Listing them individually seems to be more common. -[assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"http://.*")] -[assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"https://.*")] -#if PARTIAL_TRUST -// Allows hosting this assembly in an ASP.NET setting. Not all applications -// will host this using ASP.NET, so this is optional. Besides, we need at least -// one optional permission to activate CAS permission shrinking. -[assembly: AspNetHostingPermission(SecurityAction.RequestOptional, Level = AspNetHostingPermissionLevel.Medium)] - -// Allows this assembly to store reporting data. -[assembly: IsolatedStorageFilePermission(SecurityAction.RequestOptional, UsageAllowed = IsolatedStorageContainment.AssemblyIsolationByUser)] - -// The following are only required for diagnostic logging (Trace.Write, Debug.Assert, etc.). -#if TRACE || DEBUG -[assembly: KeyContainerPermission(SecurityAction.RequestOptional, Unrestricted = true)] -[assembly: ReflectionPermission(SecurityAction.RequestOptional, MemberAccess = true)] -[assembly: RegistryPermission(SecurityAction.RequestOptional, Unrestricted = true)] -[assembly: SecurityPermission(SecurityAction.RequestOptional, ControlEvidence = true, UnmanagedCode = true, ControlThread = true)] -[assembly: FileIOPermission(SecurityAction.RequestOptional, AllFiles = FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read)] -#endif // TRACE || DEBUG -#endif // PARTIAL_TRUST -#endif // CLR4 diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs index c4421c4..612845f 100644 --- a/src/DotNetOpenAuth/Reporting.cs +++ b/src/DotNetOpenAuth/Reporting.cs @@ -151,6 +151,7 @@ namespace DotNetOpenAuth { /// </summary> /// <param name="eventName">Name of the event.</param> /// <param name="category">The category within the event. Null and empty strings are allowed, but considered the same.</param> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "PersistentCounter instances are stored in a table for later use.")] internal static void RecordEventOccurrence(string eventName, string category) { Contract.Requires(!String.IsNullOrEmpty(eventName)); @@ -318,6 +319,7 @@ namespace DotNetOpenAuth { /// <summary> /// Initializes Reporting if it has not been initialized yet. /// </summary> + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This method must never throw.")] private static void Initialize() { lock (initializationSync) { if (!broken && !initialized) { @@ -355,44 +357,49 @@ namespace DotNetOpenAuth { /// <returns>A stream that contains the report.</returns> private static Stream GetReport() { var stream = new MemoryStream(); - var writer = new StreamWriter(stream, Encoding.UTF8); - writer.WriteLine(reportOriginIdentity.ToString("B")); - writer.WriteLine(Util.LibraryVersion); - writer.WriteLine(".NET Framework {0}", Environment.Version); - - foreach (var observation in observations) { - observation.Flush(); - writer.WriteLine("===================================="); - writer.WriteLine(observation.FileName); - try { - using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { - writer.Flush(); - fileStream.CopyTo(writer.BaseStream); + try { + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.WriteLine(reportOriginIdentity.ToString("B")); + writer.WriteLine(Util.LibraryVersion); + writer.WriteLine(".NET Framework {0}", Environment.Version); + + foreach (var observation in observations) { + observation.Flush(); + writer.WriteLine("===================================="); + writer.WriteLine(observation.FileName); + try { + using (var fileStream = new IsolatedStorageFileStream(observation.FileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } + } catch (FileNotFoundException) { + writer.WriteLine("(missing)"); } - } catch (FileNotFoundException) { - writer.WriteLine("(missing)"); } - } - // Not all event counters may have even loaded in this app instance. - // We flush the ones in memory, and then read all of them off disk. - foreach (var counter in events.Values) { - counter.Flush(); - } + // Not all event counters may have even loaded in this app instance. + // We flush the ones in memory, and then read all of them off disk. + foreach (var counter in events.Values) { + counter.Flush(); + } - foreach (string eventFile in file.GetFileNames("event-*.txt")) { - writer.WriteLine("===================================="); - writer.WriteLine(eventFile); - using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { - writer.Flush(); - fileStream.CopyTo(writer.BaseStream); + foreach (string eventFile in file.GetFileNames("event-*.txt")) { + writer.WriteLine("===================================="); + writer.WriteLine(eventFile); + using (var fileStream = new IsolatedStorageFileStream(eventFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, file)) { + writer.Flush(); + fileStream.CopyTo(writer.BaseStream); + } } - } - // Make sure the stream is positioned at the beginning. - writer.Flush(); - stream.Position = 0; - return stream; + // Make sure the stream is positioned at the beginning. + writer.Flush(); + stream.Position = 0; + return stream; + } catch { + stream.Dispose(); + throw; + } } /// <summary> diff --git a/src/DotNetOpenAuth/Util.cs b/src/DotNetOpenAuth/Util.cs index 9f8b30c..8a18ef8 100644 --- a/src/DotNetOpenAuth/Util.cs +++ b/src/DotNetOpenAuth/Util.cs @@ -126,7 +126,7 @@ namespace DotNetOpenAuth { sb.Append("\t"); sb.Append(objString); - if (!objString.EndsWith(Environment.NewLine)) { + if (!objString.EndsWith(Environment.NewLine, StringComparison.Ordinal)) { sb.AppendLine(); } sb.AppendLine("}, {"); diff --git a/src/DotNetOpenAuth/XrdsPublisher.cs b/src/DotNetOpenAuth/XrdsPublisher.cs index e7c04d8..83d82ff 100644 --- a/src/DotNetOpenAuth/XrdsPublisher.cs +++ b/src/DotNetOpenAuth/XrdsPublisher.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth { using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; using System.Drawing.Design; using System.Text; using System.Web; @@ -209,6 +210,7 @@ namespace DotNetOpenAuth { /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] protected override void Render(HtmlTextWriter writer) { + Contract.Assume(writer != null, "Missing contract."); if (this.Enabled && this.Visible && !string.IsNullOrEmpty(this.XrdsUrl)) { Uri xrdsAddress = new Uri(MessagingUtilities.GetRequestUrlFromContext(), Page.Response.ApplyAppPathModifier(this.XrdsUrl)); if ((this.XrdsAdvertisement & XrdsUrlLocations.HttpHeader) != 0) { diff --git a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs index 01dae40..06c6fc7 100644 --- a/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs +++ b/src/DotNetOpenAuth/Yadis/DiscoveryResult.cs @@ -17,6 +17,12 @@ namespace DotNetOpenAuth.Yadis { /// </summary> internal class DiscoveryResult { /// <summary> + /// The original web response, backed up here if the final web response is the preferred response to use + /// in case it turns out to not work out. + /// </summary> + private CachedDirectWebResponse htmlFallback; + + /// <summary> /// Initializes a new instance of the <see cref="DiscoveryResult"/> class. /// </summary> /// <param name="requestUri">The user-supplied identifier.</param> @@ -25,10 +31,8 @@ namespace DotNetOpenAuth.Yadis { public DiscoveryResult(Uri requestUri, CachedDirectWebResponse initialResponse, CachedDirectWebResponse finalResponse) { this.RequestUri = requestUri; this.NormalizedUri = initialResponse.FinalUri; - if (finalResponse == null) { - this.ContentType = initialResponse.ContentType; - this.ResponseText = initialResponse.GetResponseString(); - this.IsXrds = this.ContentType != null && this.ContentType.MediaType == ContentTypes.Xrds; + if (finalResponse == null || finalResponse.Status != System.Net.HttpStatusCode.OK) { + this.ApplyHtmlResponse(initialResponse); } else { this.ContentType = finalResponse.ContentType; this.ResponseText = finalResponse.GetResponseString(); @@ -36,6 +40,9 @@ namespace DotNetOpenAuth.Yadis { if (initialResponse != finalResponse) { this.YadisLocation = finalResponse.RequestUri; } + + // Back up the initial HTML response in case the XRDS is not useful. + this.htmlFallback = initialResponse; } } @@ -77,13 +84,23 @@ namespace DotNetOpenAuth.Yadis { public bool IsXrds { get; private set; } /// <summary> - /// Gets a value indicating whether discovery resulted in an - /// XRDS document at a referred location. + /// Reverts to the HTML response after the XRDS response didn't work out. + /// </summary> + internal void TryRevertToHtmlResponse() { + if (this.htmlFallback != null) { + this.ApplyHtmlResponse(this.htmlFallback); + this.htmlFallback = null; + } + } + + /// <summary> + /// Applies the HTML response to the object. /// </summary> - /// <value><c>true</c> if the response to the userSuppliedIdentifier - /// pointed to a different URL for the XRDS document.</value> - public bool UsedYadisLocation { - get { return this.YadisLocation != null; } + /// <param name="initialResponse">The initial response.</param> + private void ApplyHtmlResponse(CachedDirectWebResponse initialResponse) { + this.ContentType = initialResponse.ContentType; + this.ResponseText = initialResponse.GetResponseString(); + this.IsXrds = this.ContentType != null && this.ContentType.MediaType == ContentTypes.Xrds; } } } diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs index f1c8be3..8b8c20f 100644 --- a/src/DotNetOpenAuth/Yadis/Yadis.cs +++ b/src/DotNetOpenAuth/Yadis/Yadis.cs @@ -39,7 +39,7 @@ namespace DotNetOpenAuth.Yadis { /// The maximum number of bytes to read from an HTTP response /// in searching for a link to a YADIS document. /// </summary> - private const int MaximumResultToScan = 1024 * 1024; + internal const int MaximumResultToScan = 1024 * 1024; /// <summary> /// Performs YADIS discovery on some identifier. @@ -96,7 +96,6 @@ namespace DotNetOpenAuth.Yadis { response2 = Request(requestHandler, url, requireSsl, ContentTypes.Xrds).GetSnapshot(MaximumResultToScan); if (response2.Status != HttpStatusCode.OK) { Logger.Yadis.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response2.Status, response2.Status, uri); - return null; } } else { Logger.Yadis.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url); |