diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj | 2 | ||||
-rw-r--r-- | src/DotNetOpenAuth.Test/OpenId/AssociationHandshakeTests.cs | 68 | ||||
-rw-r--r-- | src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs | 105 | ||||
-rw-r--r-- | src/DotNetOpenAuth.Test/OpenId/RealmTests.cs | 224 | ||||
-rw-r--r-- | src/DotNetOpenAuth.sln | 9 | ||||
-rw-r--r-- | src/DotNetOpenAuth/DotNetOpenAuth.csproj | 2 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs | 154 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs | 27 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/OpenIdStrings.resx | 9 | ||||
-rw-r--r-- | src/DotNetOpenAuth/OpenId/Realm.cs | 339 | ||||
-rw-r--r-- | src/DotNetOpenAuth/UriUtil.cs | 25 |
11 files changed, 962 insertions, 2 deletions
diff --git a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj index 4469c17..6e294e5 100644 --- a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj +++ b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj @@ -102,11 +102,13 @@ <Compile Include="OpenId\Messages\AssociateUnsuccessfulResponseTests.cs" /> <Compile Include="OpenId\Messages\AssociateUnencryptedResponseTests.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdChannelTests.cs" /> + <Compile Include="OpenId\Messages\CheckIdRequestTests.cs" /> <Compile Include="OpenId\Messages\DirectErrorResponseTests.cs" /> <Compile Include="OpenId\Messages\IndirectErrorResponseTests.cs" /> <Compile Include="OpenId\OpenIdCoordinator.cs" /> <Compile Include="OpenId\AssociationHandshakeTests.cs" /> <Compile Include="OpenId\OpenIdTestBase.cs" /> + <Compile Include="OpenId\RealmTests.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Messaging\ResponseTests.cs" /> <Compile Include="OAuth\AppendixScenarios.cs" /> diff --git a/src/DotNetOpenAuth.Test/OpenId/AssociationHandshakeTests.cs b/src/DotNetOpenAuth.Test/OpenId/AssociationHandshakeTests.cs index 054a149..7743ccc 100644 --- a/src/DotNetOpenAuth.Test/OpenId/AssociationHandshakeTests.cs +++ b/src/DotNetOpenAuth.Test/OpenId/AssociationHandshakeTests.cs @@ -35,6 +35,74 @@ namespace DotNetOpenAuth.Test.OpenId { } /// <summary> + /// Verifies that the RP and OP can renegotiate an association type if the RP's + /// initial request for an association is for a type the OP doesn't support. + /// </summary> + [TestMethod, Ignore] + public void AssociateRenegotiateBitLength() { + // TODO: test where the RP asks for an association type that the OP doesn't support + throw new NotImplementedException(); + } + + /// <summary> + /// Verifies that the RP cannot get caught in an infinite loop if a bad OP + /// keeps sending it association retry messages. + /// </summary> + [TestMethod, Ignore] + public void AssociateRenegotiateBitLengthRPStopsAfterOneRetry() { + // TODO: code here + throw new NotImplementedException(); + } + + /// <summary> + /// Verifies security settings limit RP's initial associate request + /// </summary> + [TestMethod, Ignore] + public void AssociateRequestDeterminedBySecuritySettings() { + // TODO: Code here + throw new NotImplementedException(); + } + + /// <summary> + /// Verifies security settings limit RP's acceptance of OP's counter-suggestion + /// </summary> + [TestMethod, Ignore] + public void AssociateRenegotiateLimitedByRPSecuritySettings() { + // TODO: Code here + throw new NotImplementedException(); + } + + /// <summary> + /// Verifies security settings limit OP's set of acceptable association types. + /// </summary> + [TestMethod, Ignore] + public void AssociateLimitedByOPSecuritySettings() { + // TODO: Code here + throw new NotImplementedException(); + } + + /// <summary> + /// Verifies the RP can recover with no association after receiving an + /// associate error response from the OP when no suggested association + /// type is included. + /// </summary> + [TestMethod, Ignore] + public void AssociateContinueAfterOpenIdError() { + // TODO: Code here + throw new NotImplementedException(); + } + + /// <summary> + /// Verifies that the RP can recover from an invalid or non-existent + /// response from the OP, for example in the HTTP timeout case. + /// </summary> + [TestMethod, Ignore] + public void AssociateContinueAfterHttpError() { + // TODO: Code here + throw new NotImplementedException(); + } + + /// <summary> /// Runs a parameterized association flow test using all supported OpenID versions. /// </summary> /// <param name="opEndpoint">The OP endpoint to simulate using.</param> diff --git a/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs b/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs new file mode 100644 index 0000000..0f970f9 --- /dev/null +++ b/src/DotNetOpenAuth.Test/OpenId/Messages/CheckIdRequestTests.cs @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckIdRequestTests.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Test.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.Messaging; + + [TestClass] + public class CheckIdRequestTests : OpenIdTestBase { + private Uri ProviderEndpoint; + private CheckIdRequest immediatev1; + private CheckIdRequest setupv1; + private CheckIdRequest immediatev2; + private CheckIdRequest setupv2; + + + [TestInitialize] + public override void SetUp() { + base.SetUp(); + + ProviderEndpoint = new Uri("http://host"); + + immediatev1 = new CheckIdRequest(Protocol.V11.Version, ProviderEndpoint, true); + setupv1 = new CheckIdRequest(Protocol.V11.Version, ProviderEndpoint, false); + + immediatev2 = new CheckIdRequest(Protocol.V20.Version, ProviderEndpoint, true); + setupv2 = new CheckIdRequest(Protocol.V20.Version, ProviderEndpoint, false); + + // Prepare all message versions so that they SHOULD be valid by default. + // In particular, V1 messages require ReturnTo. + immediatev1.ReturnTo = new Uri("http://returnto/"); + setupv1.ReturnTo = new Uri("http://returnto/"); + + try { + immediatev1.EnsureValidMessage(); + setupv1.EnsureValidMessage(); + immediatev2.EnsureValidMessage(); + setupv2.EnsureValidMessage(); + } catch (ProtocolException ex) { + Assert.Inconclusive("All messages ought to be valid before tests run, but got: {0}", ex.Message); + } + } + + /// <summary> + /// Tests that having <see cref="CheckIdRequest.ClaimedIdentifier"/> set without + /// <see cref="CheckIdRequest.LocalIdentifier"/> set is recognized as an error in OpenID 2.x. + /// </summary> + [TestMethod, ExpectedException(typeof(ProtocolException))] + public void ClaimedIdentifierWithoutIdentity() { + setupv2.ClaimedIdentifier = "http://andrew.arnott.myopenid.com/"; + setupv2.EnsureValidMessage(); + } + + /// <summary> + /// Tests that having <see cref="CheckIdRequest.LocalIdentifier"/> set without + /// <see cref="CheckIdRequest.ClaimedIdentifier"/> set is recognized as an error in OpenID 2.x. + /// </summary> + [TestMethod, ExpectedException(typeof(ProtocolException))] + public void LocalIdentifierWithoutClaimedIdentifier() { + setupv2.LocalIdentifier = "http://andrew.arnott.myopenid.com/"; + setupv2.EnsureValidMessage(); + } + + /// <summary> + /// Tests that having <see cref="CheckIdRequest.LocalIdentifier"/> set without + /// <see cref="CheckIdRequest.ClaimedIdentifier"/> set is recognized as valid in OpenID 1.x. + /// </summary> + [TestMethod] + public void LocalIdentifierWithoutClaimedIdentifierV1() { + setupv1.LocalIdentifier = "http://andrew.arnott.myopenid.com/"; + setupv1.EnsureValidMessage(); + } + + /// <summary> + /// Verifies that the validation check throws if the return_to and the realm + /// values are not compatible. + /// </summary> + [TestMethod, ExpectedException(typeof(ProtocolException))] + public void RealmReturnToMismatchV2() { + setupv2.Realm = "http://somehost/"; + setupv2.ReturnTo = new Uri("http://someotherhost/"); + setupv2.EnsureValidMessage(); + } + + /// <summary> + /// Verifies that the validation check throws if the return_to and the realm + /// values are not compatible. + /// </summary> + [TestMethod, ExpectedException(typeof(ProtocolException))] + public void RealmReturnToMismatchV1() { + setupv1.Realm = "http://somehost/"; + setupv1.ReturnTo = new Uri("http://someotherhost/"); + setupv1.EnsureValidMessage(); + } + } +} diff --git a/src/DotNetOpenAuth.Test/OpenId/RealmTests.cs b/src/DotNetOpenAuth.Test/OpenId/RealmTests.cs new file mode 100644 index 0000000..5d59ded --- /dev/null +++ b/src/DotNetOpenAuth.Test/OpenId/RealmTests.cs @@ -0,0 +1,224 @@ +//----------------------------------------------------------------------- +// <copyright file="RealmTests.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Test +{ + using System; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using DotNetOpenAuth.OpenId; + + [TestClass] + public class RealmTests + { + [TestMethod] + public void ValidRealmsTest() + { + // Just create these. If any are determined to be invalid, + // an exception should be thrown that would fail this test. + new Realm("http://www.myopenid.com"); + new Realm("http://www.myopenid.com/"); + new Realm("http://www.myopenid.com:5221/"); + new Realm("https://www.myopenid.com"); + new Realm("http://www.myopenid.com/abc"); + new Realm("http://www.myopenid.com/abc/"); + new Realm("http://*.myopenid.com/"); + new Realm("http://*.com/"); + new Realm("http://*.guest.myopenid.com/"); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void InvalidRealmNullString() + { + new Realm((string)null); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void InvalidRealmNullUri() { + new Realm((Uri)null); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmEmpty() + { + new Realm(string.Empty); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmBadProtocol() + { + new Realm("asdf://www.microsoft.com/"); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmNoScheme() + { + new Realm("www.guy.com"); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmBadWildcard1() + { + new Realm("http://*www.my.com"); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmBadWildcard2() { + new Realm("http://www.*.com"); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmBadWildcard3() { + new Realm("http://www.my.*/"); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmTwoWildcards1() + { + new Realm("http://**.my.com"); + } + + [TestMethod] + [ExpectedException(typeof(UriFormatException))] + public void InvalidRealmTwoWildcards2() + { + new Realm("http://*.*.my.com"); + } + + [TestMethod] + public void IsSaneTest() + { + Assert.IsTrue(new Realm("http://www.myopenid.com").IsSane); + Assert.IsTrue(new Realm("http://myopenid.com").IsSane); + Assert.IsTrue(new Realm("http://localhost").IsSane); + Assert.IsTrue(new Realm("http://localhost:33532/dab").IsSane); + Assert.IsTrue(new Realm("http://www.myopenid.com").IsSane); + + Assert.IsFalse(new Realm("http://*.com").IsSane); + Assert.IsFalse(new Realm("http://*.co.uk").IsSane); + } + + [TestMethod] + public void IsUrlWithinRealmTests() + { + /* + * The openid.return_to URL MUST descend from the openid.trust_root, or the + * Identity Provider SHOULD return an error. Namely, the URL scheme and port + * MUST match. The path, if present, MUST be equal to or below the value of + * openid.trust_root, and the domains on both MUST match, or, the + * openid.trust_root value contain a wildcard like http://*.example.com. + * The wildcard SHALL only be at the beginning. It is RECOMMENDED Identity + * Provider's protect their End Users from requests for things like + * http://*.com/ or http://*.co.uk/. + */ + + // Schemes must match + Assert.IsFalse(new Realm("https://www.my.com/").Contains("http://www.my.com/")); + Assert.IsFalse(new Realm("http://www.my.com/").Contains("https://www.my.com/")); + + // Ports must match + Assert.IsTrue(new Realm("http://www.my.com/").Contains("http://www.my.com:80/boo")); + Assert.IsTrue(new Realm("http://www.my.com:80/").Contains("http://www.my.com/boo")); + Assert.IsFalse(new Realm("http://www.my.com:79/").Contains("http://www.my.com/boo")); + Assert.IsFalse(new Realm("https://www.my.com/").Contains("http://www.my.com:79/boo")); + + // Path must be (at or) below trust root + Assert.IsTrue(new Realm("http://www.my.com/").Contains("http://www.my.com/")); + Assert.IsTrue(new Realm("http://www.my.com/").Contains("http://www.my.com/boo")); + Assert.IsTrue(new Realm("http://www.my.com/p/").Contains("http://www.my.com/p/l")); + Assert.IsTrue(new Realm("http://www.my.com/bah").Contains("http://www.my.com/bah/bah")); + Assert.IsTrue(new Realm("http://www.my.com/bah").Contains("http://www.my.com/bah/bah")); + Assert.IsTrue(new Realm("http://www.my.com/bah.html").Contains("http://www.my.com/bah.html/bah")); + Assert.IsFalse(new Realm("http://www.my.com/bah").Contains("http://www.my.com/bahbah")); + Assert.IsTrue(new Realm("http://www.my.com/bah").Contains("http://www.my.com/bah?q=a")); + Assert.IsTrue(new Realm("http://www.my.com/bah?q=a").Contains("http://www.my.com/bah?q=a")); + Assert.IsTrue(new Realm("http://www.my.com/bah?a=b&c=d").Contains("http://www.my.com/bah?a=b&c=d&e=f")); + Assert.IsFalse(new Realm("http://www.my.com/bah?a=b&c=d").Contains("http://www.my.com/bah?a=b")); + + // Domains MUST match + Assert.IsFalse(new Realm("http://www.my.com/").Contains("http://yours.com/")); + Assert.IsFalse(new Realm("http://www.my.com/").Contains("http://www.yours.com/")); + Assert.IsFalse(new Realm("http://www.my.com/").Contains("http://q.www.my.com/")); + Assert.IsFalse(new Realm("http://www.my.com/").Contains("http://wwww.my.com/")); + Assert.IsFalse(new Realm("http://www.my.com/").Contains("http://www.my.com.uk/")); + Assert.IsFalse(new Realm("http://www.my.com/").Contains("http://www.my.comm/")); + + // Allow for wildcards + Assert.IsTrue(new Realm("http://*.www.my.com/").Contains("http://bah.www.my.com/")); + Assert.IsTrue(new Realm("http://*.www.my.com/").Contains("http://bah.WWW.MY.COM/")); + Assert.IsTrue(new Realm("http://*.www.my.com/").Contains("http://bah.www.my.com/boo")); + Assert.IsTrue(new Realm("http://*.my.com/").Contains("http://bah.www.my.com/boo")); + Assert.IsTrue(new Realm("http://*.my.com/").Contains("http://my.com/boo")); + Assert.IsFalse(new Realm("http://*.my.com/").Contains("http://ohmeohmy.com/")); + Assert.IsFalse(new Realm("http://*.my.com/").Contains("http://me.com/")); + Assert.IsFalse(new Realm("http://*.my.com/").Contains("http://my.co/")); + Assert.IsFalse(new Realm("http://*.my.com/").Contains("http://com/")); + Assert.IsFalse(new Realm("http://*.www.my.com/").Contains("http://my.com/")); + Assert.IsFalse(new Realm("http://*.www.my.com/").Contains("http://zzz.my.com/")); + // These are tested against by the constructor test, as these are invalid wildcard positions. + ////Assert.IsFalse(new Realm("http://*www.my.com/").ValidateUrl("http://bah.www.my.com/")); + ////Assert.IsFalse(new Realm("http://*www.my.com/").ValidateUrl("http://wwww.my.com/")); + + // Among those that should return true, mix up character casing to test for case sensitivity. + // Host names should be case INSENSITIVE, but paths should probably be case SENSITIVE, + // because in some systems they are case sensitive and to ignore this would open + // security holes. + Assert.IsTrue(new Realm("http://www.my.com/").Contains("http://WWW.MY.COM/")); + Assert.IsFalse(new Realm("http://www.my.com/abc").Contains("http://www.my.com/ABC")); + } + + [TestMethod] + public void ImplicitConversionFromStringTests() { + Realm realm = "http://host"; + Assert.AreEqual("host", realm.Host); + realm = (string)null; + Assert.IsNull(realm); + } + + [TestMethod] + public void ImplicitConversionToStringTests() { + Realm realm = new Realm("http://host/"); + string realmString = realm; + Assert.AreEqual("http://host/", realmString); + realm = null; + realmString = realm; + Assert.IsNull(realmString); + } + + [TestMethod] + public void ImplicitConverstionFromUriTests() { + Uri uri = new Uri("http://host"); + Realm realm = uri; + Assert.AreEqual(uri.Host, realm.Host); + uri = null; + realm = uri; + Assert.IsNull(realm); + } + + [TestMethod] + public void EqualsTest() + { + Realm testRealm1a = new Realm("http://www.yahoo.com"); + Realm testRealm1b = new Realm("http://www.yahoo.com"); + Realm testRealm2 = new Realm("http://www.yahoo.com/b"); + Realm testRealm3 = new Realm("http://*.www.yahoo.com"); + + Assert.AreEqual(testRealm1a, testRealm1b); + Assert.AreNotEqual(testRealm1a, testRealm2); + Assert.AreNotEqual(testRealm1a, null); + Assert.AreNotEqual(testRealm1a, testRealm1a.ToString(), "Although the URLs are equal, different object types shouldn't be equal."); + Assert.AreNotEqual(testRealm3, testRealm1a, "Wildcard difference ignored by Equals"); + } + } +} diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index a31ac97..2656006 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -14,9 +14,14 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specs", "Specs", "{CD57219F-24F4-4136-8741-6063D0D7A031}" ProjectSection(SolutionItems) = preProject ..\doc\specs\OAuth Core 1.0.htm = ..\doc\specs\OAuth Core 1.0.htm + ..\doc\specs\openid-attribute-exchange-1_0.html = ..\doc\specs\openid-attribute-exchange-1_0.html + ..\doc\specs\openid-authentication-1_1.html = ..\doc\specs\openid-authentication-1_1.html + ..\doc\specs\openid-authentication-2_0.html = ..\doc\specs\openid-authentication-2_0.html + ..\doc\specs\openid-provider-authentication-policy-extension-1_0-02.html = ..\doc\specs\openid-provider-authentication-policy-extension-1_0-02.html + ..\doc\specs\openid-simple-registration-extension-1_0.html = ..\doc\specs\openid-simple-registration-extension-1_0.html EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Consumer", "..\samples\Consumer", "{F9076F04-17AF-4205-93A2-1D3BEBFCDAEB}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Consumer", "..\samples\Consumer\", "{F9076F04-17AF-4205-93A2-1D3BEBFCDAEB}" ProjectSection(WebsiteProperties) = preProject TargetFramework = "3.5" ProjectReferences = "{3191B653-F76D-4C1A-9A5A-347BC3AAAAB7}|DotNetOpenAuth.dll;{AA78D112-D889-414B-A7D4-467B34C7B663}|DotNetOpenAuth.ApplicationBlock.dll;" @@ -45,7 +50,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{B4C6 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsumerWpf", "..\samples\ConsumerWpf\ConsumerWpf.csproj", "{6EC36418-DBC5-4AD1-A402-413604AA7A08}" EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "ServiceProvider", "..\samples\ServiceProvider", "{EC910270-AAB6-4AC6-9B57-99118CFBE557}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "ServiceProvider", "..\samples\ServiceProvider\", "{EC910270-AAB6-4AC6-9B57-99118CFBE557}" ProjectSection(WebsiteProperties) = preProject TargetFramework = "3.5" ProjectReferences = "{3191B653-F76D-4C1A-9A5A-347BC3AAAAB7}|DotNetOpenAuth.dll;" diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 0da6de6..0d143eb 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -163,6 +163,8 @@ <Compile Include="OpenId\ChannelElements\OpenIdChannel.cs" /> <Compile Include="OpenId\ChannelElements\OpenIdMessageFactory.cs" /> <Compile Include="OpenId\Configuration.cs" /> + <Compile Include="OpenId\Messages\CheckIdRequest.cs" /> + <Compile Include="OpenId\Realm.cs" /> <Compile Include="OpenId\RelyingPartyDescription.cs" /> <Compile Include="OpenId\DiffieHellmanUtilities.cs" /> <Compile Include="OpenId\DiffieHellman\DHKeyGeneration.cs" /> diff --git a/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs b/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs new file mode 100644 index 0000000..034dd65 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Messages/CheckIdRequest.cs @@ -0,0 +1,154 @@ +//----------------------------------------------------------------------- +// <copyright file="CheckIdRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An authentication request from a Relying Party to a Provider. + /// </summary> + /// <remarks> + /// This message type satisfies OpenID 2.0 section 9.1. + /// </remarks> + [DebuggerDisplay("OpenID {ProtocolVersion} {Mode} {ClaimedIdentifier}")] + internal class CheckIdRequest : RequestBase { + /// <summary> + /// Initializes a new instance of the <see cref="CheckIdRequest"/> class. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="providerEndpoint">The Provider endpoint that receives this message.</param> + /// <param name="immediate"> + /// <c>true</c> for asynchronous javascript clients; + /// <c>false</c> to allow the Provider to interact with the user in order to complete authentication. + /// </param> + internal CheckIdRequest(Version version, Uri providerEndpoint, bool immediate) : + base(version, providerEndpoint, GetMode(version, immediate), DotNetOpenAuth.Messaging.MessageTransport.Indirect) { + } + + /// <summary> + /// Gets or sets the Claimed Identifier. + /// </summary> + /// <remarks> + /// <para>"openid.claimed_id" and "openid.identity" SHALL be either both present or both absent. + /// If neither value is present, the assertion is not about an identifier, + /// and will contain other information in its payload, using extensions (Extensions). </para> + /// <para>It is RECOMMENDED that OPs accept XRI identifiers with or without the "xri://" prefix, as specified in the Normalization (Normalization) section. </para> + /// </remarks> + [MessagePart("openid.claimed_id", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + internal string ClaimedIdentifier { get; set; } + + /// <summary> + /// Gets or sets the OP Local Identifier. + /// </summary> + /// <value>The OP-Local Identifier. </value> + /// <remarks> + /// <para>If a different OP-Local Identifier is not specified, the claimed + /// identifier MUST be used as the value for openid.identity.</para> + /// <para>Note: If this is set to the special value + /// "http://specs.openid.net/auth/2.0/identifier_select" then the OP SHOULD + /// choose an Identifier that belongs to the end user. This parameter MAY + /// be omitted if the request is not about an identifier (for instance if + /// an extension is in use that makes the request meaningful without it; + /// see openid.claimed_id above). </para> + /// </remarks> + [MessagePart("openid.identity", IsRequired = false, AllowEmpty = false)] + internal string LocalIdentifier { get; set; } + + /// <summary> + /// Gets or sets the handle of the association the RP would like the Provider + /// to use for signing a positive assertion in the response message. + /// </summary> + /// <value>A handle for an association between the Relying Party and the OP + /// that SHOULD be used to sign the response. </value> + /// <remarks> + /// If no association handle is sent, the transaction will take place in Stateless Mode + /// (Verifying Directly with the OpenID Provider). + /// </remarks> + [MessagePart("openid.assoc_handle", IsRequired = false, AllowEmpty = false)] + internal string AssociationHandle { get; set; } + + /// <summary> + /// Gets or sets the URL the Provider should redirect the user agent to following + /// the authentication attempt. + /// </summary> + /// <value>URL to which the OP SHOULD return the User-Agent with the response + /// indicating the status of the request.</value> + /// <remarks> + /// <para>If this value is not sent in the request it signifies that the Relying Party + /// does not wish for the end user to be returned. </para> + /// <para>The return_to URL MAY be used as a mechanism for the Relying Party to attach + /// context about the authentication request to the authentication response. + /// This document does not define a mechanism by which the RP can ensure that query + /// parameters are not modified by outside parties; such a mechanism can be defined + /// by the RP itself. </para> + /// </remarks> + [MessagePart("openid.return_to", IsRequired = true, AllowEmpty = false)] + [MessagePart("openid.return_to", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + internal Uri ReturnTo { get; set; } + + /// <summary> + /// Gets or sets the Relying Party discovery URL the Provider may use to verify the + /// source of the authentication request. + /// </summary> + /// <value> + /// URL pattern the OP SHOULD ask the end user to trust. See Section 9.2 (Realms). + /// This value MUST be sent if openid.return_to is omitted. + /// Default: The <see cref="ReturnTo"/> URL. + /// </value> + [MessagePart("openid.trust_root", IsRequired = false, AllowEmpty = false)] + [MessagePart("openid.realm", IsRequired = false, AllowEmpty = false, MinVersion = "2.0")] + internal Realm Realm { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + public override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.ProtocolVersion.Major >= 2) { + ErrorUtilities.VerifyProtocol((this.ClaimedIdentifier == null) == (this.LocalIdentifier == null), OpenIdStrings.ClaimedIdAndLocalIdMustBothPresentOrAbsent); + } + + if (this.Realm == null) { + // Set the default Realm per the spec if it is not explicitly given. + this.Realm = this.ReturnTo; + } else if (this.ReturnTo != null) { + // Verify that the realm and return_to agree. + ErrorUtilities.VerifyProtocol(this.Realm.Contains(this.ReturnTo), OpenIdStrings.ReturnToNotUnderRealm, this.ReturnTo, this.Realm); + } + } + + /// <summary> + /// Gets the value of the openid.mode parameter based on the protocol version and immediate flag. + /// </summary> + /// <param name="version">The OpenID version to use.</param> + /// <param name="immediate"> + /// <c>true</c> for asynchronous javascript clients; + /// <c>false</c> to allow the Provider to interact with the user in order to complete authentication. + /// </param> + /// <returns>checkid_immediate or checkid_setup</returns> + private static string GetMode(Version version, bool immediate) { + ErrorUtilities.VerifyArgumentNotNull(version, "version"); + + Protocol protocol = Protocol.Lookup(version); + return immediate ? protocol.Args.Mode.checkid_immediate : protocol.Args.Mode.checkid_setup; + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index 4924d68..d6b6de0 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -88,6 +88,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to The openid.claimed_id and openid.identity parameters must both be present or both be absent.. + /// </summary> + internal static string ClaimedIdAndLocalIdMustBothPresentOrAbsent { + get { + return ResourceManager.GetString("ClaimedIdAndLocalIdMustBothPresentOrAbsent", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}. /// </summary> internal static string DiffieHellmanRequiredPropertiesNotSet { @@ -115,6 +124,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to The scheme must be http or https but was '{0}'.. + /// </summary> + internal static string InvalidScheme { + get { + return ResourceManager.GetString("InvalidScheme", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The list of keys do not match the provided dictionary.. /// </summary> internal static string KeysListAndDictionaryDoNotMatch { @@ -158,5 +176,14 @@ namespace DotNetOpenAuth.OpenId { return ResourceManager.GetString("NoSessionTypeFound", resourceCulture); } } + + /// <summary> + /// Looks up a localized string similar to return_to '{0}' not under realm '{1}'.. + /// </summary> + internal static string ReturnToNotUnderRealm { + get { + return ResourceManager.GetString("ReturnToNotUnderRealm", resourceCulture); + } + } } } diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index 3df53ac..aae4bad 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -126,6 +126,9 @@ <data name="BadAssociationPrivateData" xml:space="preserve"> <value>The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.</value> </data> + <data name="ClaimedIdAndLocalIdMustBothPresentOrAbsent" xml:space="preserve"> + <value>The openid.claimed_id and openid.identity parameters must both be present or both be absent.</value> + </data> <data name="DiffieHellmanRequiredPropertiesNotSet" xml:space="preserve"> <value>The following properties must be set before the Diffie-Hellman algorithm can generate a public key: {0}</value> </data> @@ -135,6 +138,9 @@ <data name="InvalidKeyValueFormCharacterMissing" xml:space="preserve"> <value>Cannot decode Key-Value Form because a line was found without a '{0}' character. (line {1}: '{2}')</value> </data> + <data name="InvalidScheme" xml:space="preserve"> + <value>The scheme must be http or https but was '{0}'.</value> + </data> <data name="KeysListAndDictionaryDoNotMatch" xml:space="preserve"> <value>The list of keys do not match the provided dictionary.</value> </data> @@ -150,4 +156,7 @@ <data name="NoSessionTypeFound" xml:space="preserve"> <value>Diffie-Hellman session type '{0}' not found for OpenID {1}.</value> </data> + <data name="ReturnToNotUnderRealm" xml:space="preserve"> + <value>return_to '{0}' not under realm '{1}'.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/Realm.cs b/src/DotNetOpenAuth/OpenId/Realm.cs new file mode 100644 index 0000000..80c0188 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Realm.cs @@ -0,0 +1,339 @@ +//----------------------------------------------------------------------- +// <copyright file="Realm.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Text.RegularExpressions; + using System.Xml; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// A trust root to validate requests and match return URLs against. + /// </summary> + /// <remarks> + /// This fills the OpenID Authentication 2.0 specification for realms. + /// See http://openid.net/specs/openid-authentication-2_0.html#realms + /// </remarks> + public class Realm { + /// <summary> + /// Implicitly converts the string-form of a URI to a <see cref="Realm"/> object. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] + [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads")] + public static implicit operator Realm(string uri) { + return uri != null ? new Realm(uri) : null; + } + + /// <summary> + /// Implicitly converts a <see cref="Uri"/> to a <see cref="Realm"/> object. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")] + public static implicit operator Realm(Uri uri) { + return uri != null ? new Realm(uri.AbsoluteUri) : null; + } + + /// <summary> + /// Implicitly converts a <see cref="Realm"/> object to its <see cref="String"/> form. + /// </summary> + public static implicit operator string(Realm realm) { + return realm != null ? realm.ToString() : null; + } + + /// <summary> + /// Instantiates a <see cref="Realm"/> from its string representation. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads"), SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + public Realm(string realmUrl) { + if (realmUrl == null) throw new ArgumentNullException("realmUrl"); + DomainWildcard = Regex.IsMatch(realmUrl, wildcardDetectionPattern); + uri = new Uri(Regex.Replace(realmUrl, wildcardDetectionPattern, m => m.Groups[1].Value)); + if (!uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && + !uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + throw new UriFormatException(string.Format(CultureInfo.CurrentCulture, + OpenIdStrings.InvalidScheme, uri.Scheme)); + } + + /// <summary> + /// Instantiates a <see cref="Realm"/> from its <see cref="Uri"/> representation. + /// </summary> + public Realm(Uri realmUrl) { + if (realmUrl == null) throw new ArgumentNullException("realmUrl"); + uri = realmUrl; + if (!uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && + !uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + throw new UriFormatException(string.Format(CultureInfo.CurrentCulture, + OpenIdStrings.InvalidScheme, uri.Scheme)); + } + + /// <summary> + /// Instantiates a <see cref="Realm"/> from its <see cref="UriBuilder"/> representation. + /// </summary> + /// <remarks> + /// This is useful because UriBuilder can construct a host with a wildcard + /// in the Host property, but once there it can't be converted to a Uri. + /// </remarks> + internal Realm(UriBuilder realmUriBuilder) + : this(safeUriBuilderToString(realmUriBuilder)) { } + static string safeUriBuilderToString(UriBuilder realmUriBuilder) { + if (realmUriBuilder == null) throw new ArgumentNullException("realmUriBuilder"); + // Note: we MUST use ToString. Uri property throws if wildcard is present. + return realmUriBuilder.ToString(); + } + + Uri uri; + const string wildcardDetectionPattern = @"^(\w+://)\*\."; + + /// <summary> + /// Whether a '*.' prefix to the hostname is used in the realm to allow + /// subdomains or hosts to be added to the URL. + /// </summary> + public bool DomainWildcard { get; private set; } + + /// <summary> + /// Gets the host component of this instance. + /// </summary> + public string Host { get { return uri.Host; } } + + /// <summary> + /// Gets the scheme name for this URI. + /// </summary> + public string Scheme { get { return uri.Scheme; } } + + /// <summary> + /// Gets the port number of this URI. + /// </summary> + public int Port { get { return uri.Port; } } + + /// <summary> + /// Gets the absolute path of the URI. + /// </summary> + public string AbsolutePath { get { return uri.AbsolutePath; } } + + /// <summary> + /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated + /// by a question mark (?). + /// </summary> + public string PathAndQuery { get { return uri.PathAndQuery; } } + + /// <summary> + /// Gets the realm URL. If the realm includes a wildcard, it is not included here. + /// </summary> + internal Uri NoWildcardUri { get { return uri; } } + + /// <summary> + /// Produces the Realm URL. If the realm URL had a wildcard in it, + /// the wildcard is replaced with a "www." prefix. + /// </summary> + /// <remarks> + /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of + /// the "www" prefix. + /// </remarks> + internal Uri UriWithWildcardChangedToWww { + get { + if (DomainWildcard) { + UriBuilder builder = new UriBuilder(NoWildcardUri); + builder.Host = "www." + builder.Host; + return builder.Uri; + } else { + return NoWildcardUri; + } + } + } + + static string[] topLevelDomains = { "com", "edu", "gov", "int", "mil", "net", "org", "biz", "info", "name", "museum", "coop", "aero", "ac", "ad", "ae", + "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", + "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", + "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo", + "fr", "ga", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", + "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", + "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "mg", "mh", "mk", "ml", "mm", + "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", + "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru", "rw", "sa", + "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th", + "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", + "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw" }; + + /// <summary> + /// This method checks the to see if a trust root represents a reasonable (sane) set of URLs. + /// </summary> + /// <remarks> + /// 'http://*.com/', for example is not a reasonable pattern, as it cannot meaningfully + /// specify the site claiming it. This function attempts to find many related examples, + /// but it can only work via heuristics. Negative responses from this method should be + /// treated as advisory, used only to alert the user to examine the trust root carefully. + /// </remarks> + internal bool IsSane { + get { + if (Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + return true; + + string[] host_parts = Host.Split('.'); + + string tld = host_parts[host_parts.Length - 1]; + + if (Array.IndexOf(topLevelDomains, tld) < 0) + return false; + + if (tld.Length == 2) { + if (host_parts.Length == 1) + return false; + + if (host_parts[host_parts.Length - 2].Length <= 3) + return host_parts.Length > 2; + } else { + return host_parts.Length > 1; + } + + return false; + } + } + + /// <summary> + /// Validates a URL against this trust root. + /// </summary> + /// <param name="url">A string specifying URL to check.</param> + /// <returns>Whether the given URL is within this trust root.</returns> + internal bool Contains(string url) { + return Contains(new Uri(url)); + } + + /// <summary> + /// Validates a URL against this trust root. + /// </summary> + /// <param name="url">The URL to check.</param> + /// <returns>Whether the given URL is within this trust root.</returns> + internal bool Contains(Uri url) { + if (url.Scheme != Scheme) + return false; + + if (url.Port != Port) + return false; + + if (!DomainWildcard) { + if (url.Host != Host) { + return false; + } + } else { + Debug.Assert(!string.IsNullOrEmpty(Host), "The host part of the Regex should evaluate to at least one char for successful parsed trust roots."); + string[] host_parts = Host.Split('.'); + string[] url_parts = url.Host.Split('.'); + + // If the domain containing the wildcard has more parts than the URL to match against, + // it naturally can't be valid. + // Unless *.example.com actually matches example.com too. + if (host_parts.Length > url_parts.Length) + return false; + + // Compare last part first and move forward. + // Maybe could be done by using EndsWith, but piecewies helps ensure that + // *.my.com doesn't match ohmeohmy.com but can still match my.com. + for (int i = 0; i < host_parts.Length; i++) { + string hostPart = host_parts[host_parts.Length - 1 - i]; + string urlPart = url_parts[url_parts.Length - 1 - i]; + if (!string.Equals(hostPart, urlPart, StringComparison.OrdinalIgnoreCase)) { + return false; + } + } + } + + // If path matches or is specified to root ... + // (deliberately case sensitive to protect security on case sensitive systems) + if (PathAndQuery.Equals(url.PathAndQuery, StringComparison.Ordinal) + || PathAndQuery.Equals("/", StringComparison.Ordinal)) + return true; + + // If trust root has a longer path, the return URL must be invalid. + if (PathAndQuery.Length > url.PathAndQuery.Length) + return false; + + // The following code assures that http://example.com/directory isn't below http://example.com/dir, + // but makes sure http://example.com/dir/ectory is below http://example.com/dir + int path_len = PathAndQuery.Length; + string url_prefix = url.PathAndQuery.Substring(0, path_len); + + if (PathAndQuery != url_prefix) + return false; + + // If trust root includes a query string ... + if (PathAndQuery.Contains("?")) { + // ... make sure return URL begins with a new argument + return url.PathAndQuery[path_len] == '&'; + } + + // Or make sure a query string is introduced or a path below trust root + return PathAndQuery.EndsWith("/", StringComparison.Ordinal) + || url.PathAndQuery[path_len] == '?' + || url.PathAndQuery[path_len] == '/'; + } + +#if TODO // TODO: Add discovery and then re-enable this code block + /// <summary> + /// Searches for an XRDS document at the realm URL, and if found, searches + /// for a description of a relying party endpoints (OpenId login pages). + /// </summary> + /// <param name="allowRedirects"> + /// Whether redirects may be followed when discovering the Realm. + /// This may be true when creating an unsolicited assertion, but must be + /// false when performing return URL verification per 2.0 spec section 9.2.1. + /// </param> + /// <returns>The details of the endpoints if found, otherwise null.</returns> + internal IEnumerable<DotNetOpenId.Provider.RelyingPartyReceivingEndpoint> Discover(bool allowRedirects) { + // Attempt YADIS discovery + DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww, false); + if (yadisResult != null) { + if (!allowRedirects && yadisResult.NormalizedUri != yadisResult.RequestUri) { + // Redirect occurred when it was not allowed. + throw new OpenIdException(string.Format(CultureInfo.CurrentCulture, + Strings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri)); + } + if (yadisResult.IsXrds) { + try { + XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText); + return xrds.FindRelyingPartyReceivingEndpoints(); + } catch (XmlException ex) { + throw new OpenIdException(Strings.InvalidXRDSDocument, ex); + } + } + } + return new RelyingPartyReceivingEndpoint[0]; + } +#endif + + /// <summary> + /// Checks whether one <see cref="Realm"/> is equal to another. + /// </summary> + public override bool Equals(object obj) { + Realm other = obj as Realm; + if (other == null) return false; + return uri.Equals(other.uri) && DomainWildcard == other.DomainWildcard; + } + + /// <summary> + /// Returns the hash code used for storing this object in a hash table. + /// </summary> + /// <returns></returns> + public override int GetHashCode() { + return uri.GetHashCode() + (DomainWildcard ? 1 : 0); + } + + /// <summary> + /// Returns the string form of this <see cref="Realm"/>. + /// </summary> + public override string ToString() { + if (DomainWildcard) { + UriBuilder builder = new UriBuilder(uri); + builder.Host = "*." + builder.Host; + return builder.ToStringWithImpliedPorts(); + } else { + return uri.AbsoluteUri; + } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth/UriUtil.cs b/src/DotNetOpenAuth/UriUtil.cs index 63f666d..3385e4e 100644 --- a/src/DotNetOpenAuth/UriUtil.cs +++ b/src/DotNetOpenAuth/UriUtil.cs @@ -9,6 +9,8 @@ namespace DotNetOpenAuth { using System.Collections.Specialized; using System.Linq; using System.Web; + using DotNetOpenAuth.Messaging; + using System.Text.RegularExpressions; /// <summary> /// Utility methods for working with URIs. @@ -42,5 +44,28 @@ namespace DotNetOpenAuth { return string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase); } + + /// <summary> + /// Equivalent to UriBuilder.ToString() but omits port # if it may be implied. + /// Equivalent to UriBuilder.Uri.ToString(), but doesn't throw an exception if the Host has a wildcard. + /// </summary> + public static string ToStringWithImpliedPorts(this UriBuilder builder) { + ErrorUtilities.VerifyArgumentNotNull(builder, "builder"); + + // We only check for implied ports on HTTP and HTTPS schemes since those + // are the only ones supported by OpenID anyway. + if ((builder.Port == 80 && string.Equals(builder.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || + (builder.Port == 443 && string.Equals(builder.Scheme, "https", StringComparison.OrdinalIgnoreCase))) { + // An implied port may be removed. + string url = builder.ToString(); + // Be really careful to only remove the first :80 or :443 so we are guaranteed + // we're removing only the port (and not something in the query string that + // looks like a port. + return Regex.Replace(url, @"^(https?://[^:]+):\d+", m => m.Groups[1].Value, RegexOptions.IgnoreCase); + } else { + // The port must be explicitly given anyway. + return builder.ToString(); + } + } } } |