summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2008-08-11 23:12:22 -0700
committerAndrew Arnott <andrewarnott@gmail.com>2008-08-11 23:12:22 -0700
commitd7da4e6f0700cff206f4e08558e354614a1a41d7 (patch)
treeb60d8ebbc205319ffd612ad8a4bd23c3d8a70eca
parent3c6bacf9f9e14e42eb19cfcd734030bb0ad3a472 (diff)
downloadDotNetOpenAuth-d7da4e6f0700cff206f4e08558e354614a1a41d7.zip
DotNetOpenAuth-d7da4e6f0700cff206f4e08558e354614a1a41d7.tar.gz
DotNetOpenAuth-d7da4e6f0700cff206f4e08558e354614a1a41d7.tar.bz2
Added enforcement of policy set by OpenIdRelyingParty.RequireSsl property.
Only simple tests so far... lots more to come to verify correct behavior.
-rw-r--r--src/DotNetOpenId.Test/Mocks/MockIdentifier.cs7
-rw-r--r--src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs19
-rw-r--r--src/DotNetOpenId.Test/TestSupport.cs35
-rw-r--r--src/DotNetOpenId.Test/UriIdentifierTests.cs77
-rw-r--r--src/DotNetOpenId.Test/XriIdentifierTests.cs9
-rw-r--r--src/DotNetOpenId/DotNetOpenId.csproj1
-rw-r--r--src/DotNetOpenId/Identifier.cs36
-rw-r--r--src/DotNetOpenId/NoDiscoveryIdentifier.cs43
-rw-r--r--src/DotNetOpenId/Realm.cs2
-rw-r--r--src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs5
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs5
-rw-r--r--src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs6
-rw-r--r--src/DotNetOpenId/Strings.Designer.cs27
-rw-r--r--src/DotNetOpenId/Strings.resx9
-rw-r--r--src/DotNetOpenId/UntrustedWebRequest.cs46
-rw-r--r--src/DotNetOpenId/UriIdentifier.cs80
-rw-r--r--src/DotNetOpenId/Util.cs8
-rw-r--r--src/DotNetOpenId/XriIdentifier.cs22
-rw-r--r--src/DotNetOpenId/Yadis/Yadis.cs25
19 files changed, 423 insertions, 39 deletions
diff --git a/src/DotNetOpenId.Test/Mocks/MockIdentifier.cs b/src/DotNetOpenId.Test/Mocks/MockIdentifier.cs
index 5e22640..d18ab1b 100644
--- a/src/DotNetOpenId.Test/Mocks/MockIdentifier.cs
+++ b/src/DotNetOpenId.Test/Mocks/MockIdentifier.cs
@@ -14,7 +14,8 @@ namespace DotNetOpenId.Test.Mocks {
IEnumerable<ServiceEndpoint> endpoints;
Identifier wrappedIdentifier;
- public MockIdentifier(Identifier wrappedIdentifier, IEnumerable<ServiceEndpoint> endpoints) {
+ public MockIdentifier(Identifier wrappedIdentifier, IEnumerable<ServiceEndpoint> endpoints)
+ : base(false) {
if (wrappedIdentifier == null) throw new ArgumentNullException("wrappedIdentifier");
if (endpoints == null) throw new ArgumentNullException("endpoints");
this.wrappedIdentifier = wrappedIdentifier;
@@ -36,6 +37,10 @@ namespace DotNetOpenId.Test.Mocks {
return this;
}
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ throw new NotSupportedException();
+ }
+
public override string ToString() {
return wrappedIdentifier.ToString();
}
diff --git a/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs b/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs
index 8501396..e611ae7 100644
--- a/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs
+++ b/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs
@@ -71,7 +71,7 @@ namespace DotNetOpenId.Test.RelyingParty {
[Test]
public void AssociationCreationWithStore() {
TestSupport.ResetStores(); // get rid of existing associations so a new one is created
-
+
OpenIdRelyingParty rp = TestSupport.CreateRelyingParty(null);
var directMessageSniffer = new DirectMessageSniffWrapper(rp.DirectMessageChannel);
rp.DirectMessageChannel = directMessageSniffer;
@@ -247,12 +247,27 @@ namespace DotNetOpenId.Test.RelyingParty {
rp.EndpointOrder = (se1, se2) => -se1.ServicePriority.Value.CompareTo(se2.ServicePriority.Value);
request = rp.CreateRequest("=MultipleEndpoint", realm, return_to);
Assert.AreEqual("https://authn.freexri.com/auth10/", request.Provider.Uri.AbsoluteUri);
-
+
// Now test the filter. Auth20 would come out on top, if we didn't select it out with the filter.
rp.EndpointOrder = OpenIdRelyingParty.DefaultEndpointOrder;
rp.EndpointFilter = (se) => se.Uri.AbsoluteUri == "https://authn.freexri.com/auth10/";
request = rp.CreateRequest("=MultipleEndpoint", realm, return_to);
Assert.AreEqual("https://authn.freexri.com/auth10/", request.Provider.Uri.AbsoluteUri);
}
+
+ private string stripScheme(string identifier) {
+ return identifier.Substring(identifier.IndexOf("://") + 3);
+ }
+
+ [Test]
+ public void RequireSslPrependsHttpsScheme() {
+ MockHttpRequest.Reset();
+ OpenIdRelyingParty rp = TestSupport.CreateRelyingParty(null);
+ rp.RequireSsl = true;
+ Identifier mockId = TestSupport.GetMockIdentifier(TestSupport.Scenarios.AutoApproval, ProtocolVersion.V20, true);
+ string noSchemeId = stripScheme(mockId);
+ var request = rp.CreateRequest(noSchemeId, TestSupport.Realm, TestSupport.ReturnTo);
+ Assert.IsTrue(request.ClaimedIdentifier.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase));
+ }
}
}
diff --git a/src/DotNetOpenId.Test/TestSupport.cs b/src/DotNetOpenId.Test/TestSupport.cs
index 665bb4f..f65cce7 100644
--- a/src/DotNetOpenId.Test/TestSupport.cs
+++ b/src/DotNetOpenId.Test/TestSupport.cs
@@ -52,31 +52,43 @@ public class TestSupport {
return new UriIdentifier(GetFullUrl("/" + OPDefaultPage, "user", scenario));
}
internal static UriIdentifier GetIdentityUrl(Scenarios scenario, ProtocolVersion providerVersion) {
+ return GetIdentityUrl(scenario, providerVersion, false);
+ }
+ internal static UriIdentifier GetIdentityUrl(Scenarios scenario, ProtocolVersion providerVersion, bool useSsl) {
return new UriIdentifier(GetFullUrl("/" + identityPage, new Dictionary<string, string> {
{ "user", scenario.ToString() },
{ "version", providerVersion.ToString() },
- }));
+ }, useSsl));
}
internal static UriIdentifier GetDirectedIdentityUrl(Scenarios scenario, ProtocolVersion providerVersion) {
+ return GetDirectedIdentityUrl(scenario, providerVersion, false);
+ }
+ internal static UriIdentifier GetDirectedIdentityUrl(Scenarios scenario, ProtocolVersion providerVersion, bool useSsl) {
return new UriIdentifier(GetFullUrl("/" + directedIdentityPage, new Dictionary<string, string> {
{ "user", scenario.ToString() },
{ "version", providerVersion.ToString() },
- }));
+ }, useSsl));
}
public static Identifier GetDelegateUrl(Scenarios scenario) {
- return new UriIdentifier(GetFullUrl("/" + scenario));
+ return GetDelegateUrl(scenario, false);
+ }
+ public static Identifier GetDelegateUrl(Scenarios scenario, bool useSsl) {
+ return new UriIdentifier(GetFullUrl("/" + scenario, null, useSsl));
}
internal static MockIdentifier GetMockIdentifier(Scenarios scenario, ProtocolVersion providerVersion) {
+ return GetMockIdentifier(scenario, providerVersion, false);
+ }
+ internal static MockIdentifier GetMockIdentifier(Scenarios scenario, ProtocolVersion providerVersion, bool useSsl) {
ServiceEndpoint se = ServiceEndpoint.CreateForClaimedIdentifier(
- GetIdentityUrl(scenario, providerVersion),
- GetDelegateUrl(scenario),
- GetFullUrl("/" + ProviderPage),
+ GetIdentityUrl(scenario, providerVersion, useSsl),
+ GetDelegateUrl(scenario, useSsl),
+ GetFullUrl("/" + ProviderPage, null, useSsl),
new string[] { Protocol.Lookup(providerVersion).ClaimedIdentifierServiceTypeURI },
10,
10
);
- return new MockIdentifier(GetIdentityUrl(scenario, providerVersion), new ServiceEndpoint[] { se });
+ return new MockIdentifier(GetIdentityUrl(scenario, providerVersion, useSsl), new ServiceEndpoint[] { se });
}
internal static MockIdentifier GetMockOPIdentifier(Scenarios scenario, UriIdentifier expectedClaimedId) {
Uri opEndpoint = GetFullUrl(DirectedProviderEndpoint, "user", scenario);
@@ -95,15 +107,16 @@ public class TestSupport {
return new MockIdentifier(GetOPIdentityUrl(scenario), new ServiceEndpoint[] { se });
}
public static Uri GetFullUrl(string url) {
- return GetFullUrl(url, null);
+ return GetFullUrl(url, null, false);
}
public static Uri GetFullUrl(string url, string key, object value) {
return GetFullUrl(url, new Dictionary<string, string> {
{ key, value.ToString() },
- });
+ }, false);
}
- public static Uri GetFullUrl(string url, IDictionary<string, string> args) {
- Uri baseUri = UITestSupport.Host != null ? UITestSupport.Host.BaseUri : new Uri("http://localhost/");
+ public static Uri GetFullUrl(string url, IDictionary<string, string> args, bool useSsl) {
+ Uri defaultUriBase = new Uri(useSsl ? "https://localhost/" : "http://localhost/");
+ Uri baseUri = UITestSupport.Host != null ? UITestSupport.Host.BaseUri : defaultUriBase;
UriBuilder builder = new UriBuilder(new Uri(baseUri, url));
UriUtil.AppendQueryArgs(builder, args);
return builder.Uri;
diff --git a/src/DotNetOpenId.Test/UriIdentifierTests.cs b/src/DotNetOpenId.Test/UriIdentifierTests.cs
index 51963f7..63b7ccd 100644
--- a/src/DotNetOpenId.Test/UriIdentifierTests.cs
+++ b/src/DotNetOpenId.Test/UriIdentifierTests.cs
@@ -47,6 +47,39 @@ namespace DotNetOpenId.Test {
public void CtorGoodUri() {
var uri = new UriIdentifier(goodUri);
Assert.AreEqual(new Uri(goodUri), uri.Uri);
+ Assert.IsFalse(uri.SchemeImplicitlyPrepended);
+ Assert.IsFalse(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [Test]
+ public void CtorStringNoSchemeSecure() {
+ var uri = new UriIdentifier("host/path", true);
+ Assert.AreEqual("https://host/path", uri.Uri.AbsoluteUri);
+ Assert.IsTrue(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [Test]
+ public void CtorStringHttpsSchemeSecure() {
+ var uri = new UriIdentifier("https://host/path", true);
+ Assert.AreEqual("https://host/path", uri.Uri.AbsoluteUri);
+ Assert.IsTrue(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [Test, ExpectedException(typeof(ArgumentException))]
+ public void CtorStringHttpSchemeSecure() {
+ new UriIdentifier("http://host/path", true);
+ }
+
+ [Test]
+ public void CtorUriHttpsSchemeSecure() {
+ var uri = new UriIdentifier(new Uri("https://host/path"), true);
+ Assert.AreEqual("https://host/path", uri.Uri.AbsoluteUri);
+ Assert.IsTrue(uri.IsDiscoverySecureEndToEnd);
+ }
+
+ [Test, ExpectedException(typeof(ArgumentException))]
+ public void CtorUriHttpSchemeSecure() {
+ new UriIdentifier(new Uri("http://host/path"), true);
}
/// <summary>
@@ -220,5 +253,49 @@ namespace DotNetOpenId.Test {
id = "https://HOST:80/PaTH?KeY=VaLUE#fRag";
Assert.AreEqual("https://host:80/PaTH?KeY=VaLUE#fRag", id.ToString());
}
+
+ [Test]
+ public void HttpSchemePrepended() {
+ UriIdentifier id = new UriIdentifier("www.yahoo.com");
+ Assert.AreEqual("http://www.yahoo.com/", id.ToString());
+ Assert.IsTrue(id.SchemeImplicitlyPrepended);
+ }
+
+ //[Test, Ignore("The spec says http:// must be prepended in this case, but that just creates an invalid URI. Our UntrustedWebRequest will stop disallowed schemes.")]
+ public void CtorDisallowedScheme() {
+ UriIdentifier id = new UriIdentifier(new Uri("ftp://host/path"));
+ Assert.AreEqual("http://ftp://host/path", id.ToString());
+ Assert.IsTrue(id.SchemeImplicitlyPrepended);
+ }
+
+ [Test]
+ public void TryRequireSsl() {
+ Identifier secureId;
+ // Try Parse and ctor without explicit scheme
+ var id = Identifier.Parse("www.yahoo.com");
+ Assert.AreEqual("http://www.yahoo.com/", id.ToString());
+ Assert.IsTrue(id.TryRequireSsl(out secureId));
+ Assert.IsTrue(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("https://www.yahoo.com/", secureId.ToString());
+
+ id = new UriIdentifier("www.yahoo.com");
+ Assert.AreEqual("http://www.yahoo.com/", id.ToString());
+ Assert.IsTrue(id.TryRequireSsl(out secureId));
+ Assert.IsTrue(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("https://www.yahoo.com/", secureId.ToString());
+
+ // Try Parse and ctor with explicit http:// scheme
+ id = Identifier.Parse("http://www.yahoo.com");
+ Assert.IsFalse(id.TryRequireSsl(out secureId));
+ Assert.IsFalse(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("http://www.yahoo.com/", secureId.ToString());
+ Assert.AreEqual(0, secureId.Discover().Count());
+
+ id = new UriIdentifier("http://www.yahoo.com");
+ Assert.IsFalse(id.TryRequireSsl(out secureId));
+ Assert.IsFalse(secureId.IsDiscoverySecureEndToEnd);
+ Assert.AreEqual("http://www.yahoo.com/", secureId.ToString());
+ Assert.AreEqual(0, secureId.Discover().Count());
+ }
}
}
diff --git a/src/DotNetOpenId.Test/XriIdentifierTests.cs b/src/DotNetOpenId.Test/XriIdentifierTests.cs
index b44554a..77f65f4 100644
--- a/src/DotNetOpenId.Test/XriIdentifierTests.cs
+++ b/src/DotNetOpenId.Test/XriIdentifierTests.cs
@@ -36,6 +36,15 @@ namespace DotNetOpenId.Test {
var xri = new XriIdentifier(goodXri);
Assert.AreEqual(goodXri, xri.OriginalXri);
Assert.AreEqual(goodXri, xri.CanonicalXri); // assumes 'goodXri' is canonical already
+ Assert.IsFalse(xri.IsDiscoverySecureEndToEnd);
+ }
+
+ [Test]
+ public void CtorGoodXriSecure() {
+ var xri = new XriIdentifier(goodXri, true);
+ Assert.AreEqual(goodXri, xri.OriginalXri);
+ Assert.AreEqual(goodXri, xri.CanonicalXri); // assumes 'goodXri' is canonical already
+ Assert.IsTrue(xri.IsDiscoverySecureEndToEnd);
}
[Test]
diff --git a/src/DotNetOpenId/DotNetOpenId.csproj b/src/DotNetOpenId/DotNetOpenId.csproj
index 525b433..67cc1e5 100644
--- a/src/DotNetOpenId/DotNetOpenId.csproj
+++ b/src/DotNetOpenId/DotNetOpenId.csproj
@@ -59,6 +59,7 @@
<Compile Include="Association.cs" />
<Compile Include="AssociationMemoryStore.cs" />
<Compile Include="Associations.cs" />
+ <Compile Include="NoDiscoveryIdentifier.cs" />
<Compile Include="Provider\SigningMessageEncoder.cs" />
<Compile Include="RelyingParty\DirectMessageHttpChannel.cs" />
<Compile Include="RelyingParty\IDirectMessageChannel.cs" />
diff --git a/src/DotNetOpenId/Identifier.cs b/src/DotNetOpenId/Identifier.cs
index 54e9c36..7fcd272 100644
--- a/src/DotNetOpenId/Identifier.cs
+++ b/src/DotNetOpenId/Identifier.cs
@@ -11,6 +11,26 @@ namespace DotNetOpenId {
/// </summary>
public abstract class Identifier {
/// <summary>
+ /// Constructs an <see cref="Identifier"/>.
+ /// </summary>
+ /// <param name="isDiscoverySecureEndToEnd">
+ /// Whether the derived class is prepared to guarantee end-to-end discovery
+ /// and initial redirect for authentication is performed using SSL.
+ /// </param>
+ protected Identifier(bool isDiscoverySecureEndToEnd) {
+ IsDiscoverySecureEndToEnd = isDiscoverySecureEndToEnd;
+ }
+
+ /// <summary>
+ /// Whether this Identifier will ensure SSL is used throughout the discovery phase
+ /// and initial redirect of authentication.
+ /// </summary>
+ /// <remarks>
+ /// If this is False, a value of True may be obtained by calling <see cref="TryRequireSsl"/>.
+ /// </remarks>
+ protected internal bool IsDiscoverySecureEndToEnd { get; private set; }
+
+ /// <summary>
/// Converts the string representation of an Identifier to its strong type.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates"), SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads")]
@@ -115,5 +135,21 @@ namespace DotNetOpenId {
/// a <see cref="UriIdentifier"/> or no fragment exists.
/// </summary>
internal abstract Identifier TrimFragment();
+
+ /// <summary>
+ /// Converts a given identifier to its secure equivalent.
+ /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS.
+ /// Discovery is made to require SSL for the entire resolution process.
+ /// </summary>
+ /// <param name="secureIdentifier">
+ /// The newly created secure identifier.
+ /// If the conversion fails, <see cref="secureIdentifier"/> retains
+ /// <i>this</i> identifiers identity, but will never discover any endpoints.
+ /// </param>
+ /// <returns>
+ /// True if the secure conversion was successful.
+ /// False if the Identifier was originally created with an explicit HTTP scheme.
+ /// </returns>
+ internal abstract bool TryRequireSsl(out Identifier secureIdentifier);
}
}
diff --git a/src/DotNetOpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenId/NoDiscoveryIdentifier.cs
new file mode 100644
index 0000000..8f772c4
--- /dev/null
+++ b/src/DotNetOpenId/NoDiscoveryIdentifier.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId {
+ /// <summary>
+ /// Wraps an existing Identifier and prevents it from performing discovery.
+ /// </summary>
+ class NoDiscoveryIdentifier : Identifier {
+ Identifier wrappedIdentifier ;
+ internal NoDiscoveryIdentifier(Identifier wrappedIdentifier)
+ : base(false) {
+ if (wrappedIdentifier == null) throw new ArgumentNullException("wrappedIdentifier");
+
+ this.wrappedIdentifier = wrappedIdentifier;
+ }
+
+ internal override IEnumerable<ServiceEndpoint> Discover() {
+ return new ServiceEndpoint[0];
+ }
+
+ internal override Identifier TrimFragment() {
+ return new NoDiscoveryIdentifier(wrappedIdentifier.TrimFragment());
+ }
+
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ return wrappedIdentifier.TryRequireSsl(out secureIdentifier);
+ }
+
+ public override string ToString() {
+ return wrappedIdentifier.ToString();
+ }
+
+ public override bool Equals(object obj) {
+ return wrappedIdentifier.Equals(obj);
+ }
+
+ public override int GetHashCode() {
+ return wrappedIdentifier.GetHashCode();
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Realm.cs b/src/DotNetOpenId/Realm.cs
index c48265e..f7bb361 100644
--- a/src/DotNetOpenId/Realm.cs
+++ b/src/DotNetOpenId/Realm.cs
@@ -268,7 +268,7 @@ namespace DotNetOpenId {
/// <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);
+ DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww, false);
if (yadisResult != null) {
if (!allowRedirects && yadisResult.NormalizedUri != yadisResult.RequestUri) {
// Redirect occurred when it was not allowed.
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
index 223769c..f024779 100644
--- a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
@@ -58,6 +58,11 @@ namespace DotNetOpenId.RelyingParty {
if (realm == null) throw new ArgumentNullException("realm");
userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment();
+ if (relyingParty.RequireSsl) {
+ // Rather than check for successful SSL conversion at this stage,
+ // We'll wait for secure discovery to fail on the new identifier.
+ userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier);
+ }
Logger.InfoFormat("Creating authentication request for user supplied Identifier: {0}",
userSuppliedIdentifier);
Logger.DebugFormat("Realm: {0}", realm);
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
index 9ad0a28..6091558 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -370,9 +370,10 @@ namespace DotNetOpenId.RelyingParty {
/// <item>Any redirects resulting from discovery on the user-supplied identifier must be HTTPS.</item>
/// <item>Any XRDS file found by discovery on the Claimed Identifier must be protected using HTTPS.</item>
/// <item>Only Provider endpoints found at HTTPS URLs will be considered.</item>
- /// <item>Only HTTPS redirects for authentication at the provider are accepted.</item>
+ /// <item>If the discovered identifier is an OP Identifier (directed identity), the
+ /// Claimed Identifier eventually asserted by the Provider must be an HTTPS identifier.</item>
/// </list>
- /// <para>Although the first redirect from this relying party to the Provider can be required
+ /// <para>Although the first redirect from this relying party to the Provider is required
/// to use HTTPS, any additional redirects within the Provider cannot be protected and MAY
/// revert the user's connection to HTTP, based on individual Provider implementation.
/// There is nothing that the RP can do to detect or prevent this.</para>
diff --git a/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs b/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs
index 6c808c1..ea0e23f 100644
--- a/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs
+++ b/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs
@@ -24,6 +24,12 @@ namespace DotNetOpenId.RelyingParty {
/// This value MUST be an absolute HTTP or HTTPS URL.
/// </remarks>
public Uri ProviderEndpoint { get; private set; }
+ /// <summary>
+ /// Returns true if the <see cref="ProviderEndpoint"/> is using an encrypted channel.
+ /// </summary>
+ internal bool IsSecure {
+ get { return string.Equals(ProviderEndpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); }
+ }
Uri IProviderEndpoint.Uri { get { return ProviderEndpoint; } }
/*
/// <summary>
diff --git a/src/DotNetOpenId/Strings.Designer.cs b/src/DotNetOpenId/Strings.Designer.cs
index c77a563..28c186f 100644
--- a/src/DotNetOpenId/Strings.Designer.cs
+++ b/src/DotNetOpenId/Strings.Designer.cs
@@ -142,6 +142,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to URI is not SSL yet requireSslDiscovery is set to true..
+ /// </summary>
+ internal static string ExplicitHttpUriSuppliedWithSslRequirement {
+ get {
+ return ResourceManager.GetString("ExplicitHttpUriSuppliedWithSslRequirement", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to An extension sharing namespace &apos;{0}&apos; has already been added. Only one extension per namespace is allowed in a given request..
/// </summary>
internal static string ExtensionAlreadyAddedWithSameTypeURI {
@@ -223,6 +232,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to Insecure web request for &apos;{0}&apos; aborted due to security requirements demanding HTTPS..
+ /// </summary>
+ internal static string InsecureWebRequestWithSslRequired {
+ get {
+ return ResourceManager.GetString("InsecureWebRequestWithSslRequired", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Cannot encode &apos;{0}&apos; because it contains an illegal character for Key-Value Form encoding. (line {1}: &apos;{2}&apos;).
/// </summary>
internal static string InvalidCharacterInKeyValueFormInput {
@@ -533,6 +551,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to The maximum allowable number of redirects were exceeded while requesting &apos;{0}&apos;..
+ /// </summary>
+ internal static string TooManyRedirects {
+ get {
+ return ResourceManager.GetString("TooManyRedirects", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The type must implement {0}..
/// </summary>
internal static string TypeMustImplementX {
diff --git a/src/DotNetOpenId/Strings.resx b/src/DotNetOpenId/Strings.resx
index 8f757b3..cdaec49 100644
--- a/src/DotNetOpenId/Strings.resx
+++ b/src/DotNetOpenId/Strings.resx
@@ -144,6 +144,9 @@
<data name="ExpiredNonce" xml:space="preserve">
<value>The nonce has expired.</value>
</data>
+ <data name="ExplicitHttpUriSuppliedWithSslRequirement" xml:space="preserve">
+ <value>URI is not SSL yet requireSslDiscovery is set to true.</value>
+ </data>
<data name="ExtensionAlreadyAddedWithSameTypeURI" xml:space="preserve">
<value>An extension sharing namespace '{0}' has already been added. Only one extension per namespace is allowed in a given request.</value>
</data>
@@ -171,6 +174,9 @@
<data name="InconsistentAppState" xml:space="preserve">
<value>Inconsistent setting of application state. Authentication request was sent with application state available, but authentication response was received without it available. This makes it impossible to validate the token's signature and will cause assertion verification failure.</value>
</data>
+ <data name="InsecureWebRequestWithSslRequired" xml:space="preserve">
+ <value>Insecure web request for '{0}' aborted due to security requirements demanding HTTPS.</value>
+ </data>
<data name="InvalidCharacterInKeyValueFormInput" xml:space="preserve">
<value>Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}')</value>
</data>
@@ -277,6 +283,9 @@ Discovered endpoint info:
<data name="TamperingDetected" xml:space="preserve">
<value>The '{0}' parameter was expected to have the value '{1}' but had '{2}' instead.</value>
</data>
+ <data name="TooManyRedirects" xml:space="preserve">
+ <value>The maximum allowable number of redirects were exceeded while requesting '{0}'.</value>
+ </data>
<data name="TypeMustImplementX" xml:space="preserve">
<value>The type must implement {0}.</value>
</data>
diff --git a/src/DotNetOpenId/UntrustedWebRequest.cs b/src/DotNetOpenId/UntrustedWebRequest.cs
index 852f21f..542c29a 100644
--- a/src/DotNetOpenId/UntrustedWebRequest.cs
+++ b/src/DotNetOpenId/UntrustedWebRequest.cs
@@ -223,11 +223,41 @@ namespace DotNetOpenId {
return Request(uri, body, acceptTypes, false);
}
- static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes,
- bool avoidSendingExpect100Continue) {
+ internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes, bool requireSsl) {
+ if (!requireSsl) {
+ // This is the simplest case... if we don't care about SSL, then let
+ // HttpWebRequest do auto-redirection.
+ return RequestInternal(uri, body, acceptTypes, false, false, uri);
+ } else {
+ // Since we require SSL for every redirect, we handle each redirect manually
+ // in order to detect and fail if any redirect sends us to an HTTP url.
+ Uri originalRequestUri = uri;
+ int i;
+ for (i = 0; i < MaximumRedirections; i++) {
+ UntrustedWebResponse response = RequestInternal(uri, body, acceptTypes, true, false, originalRequestUri);
+ Debug.Assert(response.RequestUri == response.FinalUri, "Redirects shouldn't have been allowed!");
+ if (response.StatusCode == HttpStatusCode.MovedPermanently ||
+ response.StatusCode == HttpStatusCode.Redirect ||
+ response.StatusCode == HttpStatusCode.RedirectMethod ||
+ response.StatusCode == HttpStatusCode.RedirectKeepVerb) {
+ uri = new Uri(response.Headers[HttpResponseHeader.Location]);
+ } else {
+ return response;
+ }
+ }
+ throw new WebException(string.Format(CultureInfo.CurrentCulture, Strings.TooManyRedirects, originalRequestUri));
+ }
+ }
+
+ static UntrustedWebResponse RequestInternal(Uri uri, byte[] body, string[] acceptTypes,
+ bool requireSslAndNoRedirects, bool avoidSendingExpect100Continue, Uri originalRequestUri) {
if (uri == null) throw new ArgumentNullException("uri");
+ if (originalRequestUri == null) throw new ArgumentNullException("originalRequestUri");
if (!isUriAllowable(uri)) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.UnsafeWebRequestDetected, uri), "uri");
+ if (requireSslAndNoRedirects && !String.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture, Strings.InsecureWebRequestWithSslRequired, uri));
+ }
// mock the request if a hosting unit test has configured it.
if (MockRequests != null) {
@@ -235,10 +265,14 @@ namespace DotNetOpenId {
}
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
+ // If SSL is required throughout, we cannot allow auto redirects because
+ // it may include a pass through an unprotected HTTP request.
+ // In the SSL required scenario, we have to follow redirects manually,
+ // and our caller will be responsible for that.
+ request.AllowAutoRedirect = !requireSslAndNoRedirects;
request.ReadWriteTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
request.Timeout = (int)Timeout.TotalMilliseconds;
request.KeepAlive = false;
- request.MaximumAutomaticRedirections = MaximumRedirections;
if (acceptTypes != null)
request.Accept = string.Join(",", acceptTypes);
if (body != null) {
@@ -266,17 +300,17 @@ namespace DotNetOpenId {
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
- return getResponse(uri, response);
+ return getResponse(originalRequestUri, response);
}
} catch (WebException e) {
using (HttpWebResponse response = (HttpWebResponse)e.Response) {
if (response != null) {
if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
if (!avoidSendingExpect100Continue) { // must only try this once more
- return Request(uri, body, acceptTypes, true);
+ return RequestInternal(uri, body, acceptTypes, requireSslAndNoRedirects, true, originalRequestUri);
}
}
- return getResponse(uri, response);
+ return getResponse(originalRequestUri, response);
} else {
throw;
}
diff --git a/src/DotNetOpenId/UriIdentifier.cs b/src/DotNetOpenId/UriIdentifier.cs
index 6371a59..b97ac96 100644
--- a/src/DotNetOpenId/UriIdentifier.cs
+++ b/src/DotNetOpenId/UriIdentifier.cs
@@ -18,21 +18,40 @@ namespace DotNetOpenId {
return new UriIdentifier(identifier);
}
- public UriIdentifier(string uri) {
+ public UriIdentifier(string uri) : this(uri, false) { }
+ public UriIdentifier(string uri, bool requireSslDiscovery)
+ : base(requireSslDiscovery) {
if (string.IsNullOrEmpty(uri)) throw new ArgumentNullException("uri");
Uri canonicalUri;
- if (!TryCanonicalize(uri, out canonicalUri))
+ bool schemePrepended;
+ if (!TryCanonicalize(uri, out canonicalUri, requireSslDiscovery, out schemePrepended))
throw new UriFormatException();
+ if (requireSslDiscovery && canonicalUri.Scheme != Uri.UriSchemeHttps) {
+ throw new ArgumentException(Strings.ExplicitHttpUriSuppliedWithSslRequirement);
+ }
Uri = canonicalUri;
+ SchemeImplicitlyPrepended = schemePrepended;
}
- public UriIdentifier(Uri uri) {
+ public UriIdentifier(Uri uri) : this(uri, false) { }
+ public UriIdentifier(Uri uri, bool requireSslDiscovery)
+ : base(requireSslDiscovery) {
if (uri == null) throw new ArgumentNullException("uri");
if (!TryCanonicalize(new UriBuilder(uri), out uri))
throw new UriFormatException();
+ if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) {
+ throw new ArgumentException(Strings.ExplicitHttpUriSuppliedWithSslRequirement);
+ }
Uri = uri;
+ SchemeImplicitlyPrepended = false;
}
public Uri Uri { get; private set; }
+ /// <summary>
+ /// Gets whether the scheme was missing when this Identifier was
+ /// created and added automatically as part of the normalization
+ /// process.
+ /// </summary>
+ internal bool SchemeImplicitlyPrepended { get; private set; }
static bool isAllowedScheme(string uri) {
if (string.IsNullOrEmpty(uri)) return false;
@@ -41,15 +60,20 @@ namespace DotNetOpenId {
}
static bool isAllowedScheme(Uri uri) {
if (uri == null) return false;
- return Array.FindIndex(allowedSchemes, s =>
+ return Array.FindIndex(allowedSchemes, s =>
uri.Scheme.Equals(s, StringComparison.OrdinalIgnoreCase)) >= 0;
}
- static bool TryCanonicalize(string uri, out Uri canonicalUri) {
+ static bool TryCanonicalize(string uri, out Uri canonicalUri, bool forceHttpsDefaultScheme, out bool schemePrepended) {
canonicalUri = null;
+ schemePrepended = false;
try {
// Assume http:// scheme if an allowed scheme isn't given, and strip
// fragments off. Consistent with spec section 7.2#3
- if (!isAllowedScheme(uri)) uri = "http" + Uri.SchemeDelimiter + uri;
+ if (!isAllowedScheme(uri)) {
+ uri = (forceHttpsDefaultScheme ? Uri.UriSchemeHttps : Uri.UriSchemeHttp) +
+ Uri.SchemeDelimiter + uri;
+ schemePrepended = true;
+ }
// Use a UriBuilder because it helps to normalize the URL as well.
return TryCanonicalize(new UriBuilder(uri), out canonicalUri);
} catch (UriFormatException) {
@@ -82,7 +106,8 @@ namespace DotNetOpenId {
}
internal static bool IsValidUri(string uri) {
Uri normalized;
- return TryCanonicalize(uri, out normalized);
+ bool schemePrepended;
+ return TryCanonicalize(uri, out normalized, false, out schemePrepended);
}
internal static bool IsValidUri(Uri uri) {
if (uri == null) return false;
@@ -150,16 +175,21 @@ namespace DotNetOpenId {
internal override IEnumerable<ServiceEndpoint> Discover() {
List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();
// Attempt YADIS discovery
- DiscoveryResult yadisResult = Yadis.Yadis.Discover(this);
+ DiscoveryResult yadisResult = Yadis.Yadis.Discover(this, IsDiscoverySecureEndToEnd);
if (yadisResult != null) {
if (yadisResult.IsXrds) {
XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
- endpoints.AddRange(xrds.CreateServiceEndpoints(yadisResult.NormalizedUri));
+ var xrdsEndpoints = xrds.CreateServiceEndpoints(yadisResult.NormalizedUri);
+ // Filter out insecure endpoints if high security is required.
+ if (IsDiscoverySecureEndToEnd) {
+ xrdsEndpoints = Util.Where(xrdsEndpoints, se => se.IsSecure);
+ }
+ endpoints.AddRange(xrdsEndpoints);
}
// Failing YADIS discovery of an XRDS document, we try HTML discovery.
if (endpoints.Count == 0) {
ServiceEndpoint ep = DiscoverFromHtml(yadisResult.NormalizedUri, yadisResult.ResponseText);
- if (ep != null) {
+ if (ep != null && (!IsDiscoverySecureEndToEnd || ep.IsSecure)) {
endpoints.Add(ep);
}
}
@@ -178,6 +208,36 @@ namespace DotNetOpenId {
return builder.Uri;
}
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ // If this Identifier is already secure, reuse it.
+ if (IsDiscoverySecureEndToEnd) {
+ secureIdentifier = this;
+ return true;
+ }
+
+ // If this identifier already uses SSL for initial discovery, return one
+ // that guarantees it will be used throughout the discovery process.
+ if (String.Equals(Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ secureIdentifier = new UriIdentifier(this.Uri, true);
+ return true;
+ }
+
+ // Otherwise, try to make this Identifier secure by normalizing to HTTPS instead of HTTP.
+ if (SchemeImplicitlyPrepended) {
+ UriBuilder newIdentifierUri = new UriBuilder(this.Uri);
+ newIdentifierUri.Scheme = Uri.UriSchemeHttps;
+ if (newIdentifierUri.Port == 80) {
+ newIdentifierUri.Port = 443;
+ }
+ secureIdentifier = new UriIdentifier(newIdentifierUri.Uri, true);
+ return true;
+ }
+
+ // This identifier is explicitly NOT https, so we cannot change it.
+ secureIdentifier = new NoDiscoveryIdentifier(this);
+ return false;
+ }
+
public override bool Equals(object obj) {
UriIdentifier other = obj as UriIdentifier;
if (other == null) return false;
diff --git a/src/DotNetOpenId/Util.cs b/src/DotNetOpenId/Util.cs
index ff2a3e2..ec6b7f6 100644
--- a/src/DotNetOpenId/Util.cs
+++ b/src/DotNetOpenId/Util.cs
@@ -265,6 +265,14 @@ namespace DotNetOpenId {
return null;
}
+ internal static IEnumerable<T> Where<T>(IEnumerable<T> sequence, Func<T, bool> predicate) {
+ foreach (T item in sequence) {
+ if (predicate(item)) {
+ yield return item;
+ }
+ }
+ }
+
/// <summary>
/// Prepares a dictionary for printing as a string.
/// </summary>
diff --git a/src/DotNetOpenId/XriIdentifier.cs b/src/DotNetOpenId/XriIdentifier.cs
index e7be251..9b49f87 100644
--- a/src/DotNetOpenId/XriIdentifier.cs
+++ b/src/DotNetOpenId/XriIdentifier.cs
@@ -12,10 +12,18 @@ namespace DotNetOpenId {
internal static readonly char[] GlobalContextSymbols = { '=', '@', '+', '$', '!' };
const string xriScheme = "xri://";
- public XriIdentifier(string xri) {
+ public XriIdentifier(string xri) : this(xri, false) { }
+ public XriIdentifier(string xri, bool requireSsl)
+ : base(requireSsl) {
if (!IsValidXri(xri))
throw new FormatException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidXri, xri));
+ xriResolverProxy = xriResolverProxyTemplate;
+ if (requireSsl) {
+ // Indicate to xri.net that we require SSL to be used for delegated resolution
+ // of community i-names.
+ xriResolverProxy += "&ssl=true";
+ }
OriginalXri = xri;
CanonicalXri = canonicalizeXri(xri);
}
@@ -56,14 +64,17 @@ namespace DotNetOpenId {
/// We use application/xrd+xml instead of application/xrds+xml because it gets
/// xri.net to automatically give us exactly the right XRD element for community i-names
/// automatically, saving us having to choose which one to use out of the result.
+ /// The ssl=true parameter tells the proxy resolver to accept only SSL connections
+ /// when resolving community i-names.
/// </remarks>
- const string xriResolverProxy = "https://xri.net/{0}?_xrd_r=application/xrd%2Bxml;sep=false";
+ const string xriResolverProxyTemplate = "https://xri.net/{0}?_xrd_r=application/xrd%2Bxml;sep=false";
+ readonly string xriResolverProxy;
/// <summary>
/// Resolves the XRI to a URL from which an XRDS document may be downloaded.
/// </summary>
protected virtual Uri XrdsUrl {
get {
- return new Uri(string.Format(CultureInfo.InvariantCulture,
+ return new Uri(string.Format(CultureInfo.InvariantCulture,
xriResolverProxy, this));
}
}
@@ -89,6 +100,11 @@ namespace DotNetOpenId {
return this;
}
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ secureIdentifier = IsDiscoverySecureEndToEnd ? this : new XriIdentifier(this, true);
+ return true;
+ }
+
public override bool Equals(object obj) {
XriIdentifier other = obj as XriIdentifier;
if (other == null) return false;
diff --git a/src/DotNetOpenId/Yadis/Yadis.cs b/src/DotNetOpenId/Yadis/Yadis.cs
index 1ae477d..710a35b 100644
--- a/src/DotNetOpenId/Yadis/Yadis.cs
+++ b/src/DotNetOpenId/Yadis/Yadis.cs
@@ -13,11 +13,26 @@ namespace DotNetOpenId.Yadis {
class Yadis {
internal const string HeaderName = "X-XRDS-Location";
- public static DiscoveryResult Discover(UriIdentifier uri) {
+ /// <summary>
+ /// Performs YADIS discovery on some identifier.
+ /// </summary>
+ /// <param name="uri">The URI to perform discovery on.</param>
+ /// <param name="requireSsl">Whether discovery should fail if any step of it is not encrypted.</param>
+ /// <returns>
+ /// The result of discovery on the given URL.
+ /// Null may be returned if an error occurs,
+ /// or if <paramref name="requireSsl"/> is true but part of discovery
+ /// is not protected by SSL.
+ /// </returns>
+ public static DiscoveryResult Discover(UriIdentifier uri, bool requireSsl) {
UntrustedWebResponse response;
try {
+ if (requireSsl && !string.Equals(uri.Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ Logger.WarnFormat("Discovery on insecure identifier '{0}' aborted.", uri);
+ return null;
+ }
response = UntrustedWebRequest.Request(uri, null,
- new[] { ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds });
+ new[] { ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds }, requireSsl);
if (response.StatusCode != System.Net.HttpStatusCode.OK) {
return null;
}
@@ -37,7 +52,11 @@ namespace DotNetOpenId.Yadis {
if (url == null && response.ContentType.MediaType == ContentTypes.Html)
url = FindYadisDocumentLocationInHtmlMetaTags(response.ReadResponseString());
if (url != null) {
- response2 = UntrustedWebRequest.Request(url);
+ if (requireSsl && !string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ Logger.WarnFormat("XRDS document at insecure location '{0}'. Aborting discovery.", url);
+ return null;
+ }
+ response2 = UntrustedWebRequest.Request(url, null, null, requireSsl);
if (response2.StatusCode != System.Net.HttpStatusCode.OK) {
return null;
}