summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.proj3
-rw-r--r--doc/logo/dotnetopenid_64x64.pngbin0 -> 3111 bytes
-rw-r--r--samples/RelyingPartyClassicAsp/MembersOnly.asp27
-rw-r--r--samples/RelyingPartyClassicAsp/default.asp39
-rw-r--r--samples/RelyingPartyClassicAsp/images/dotnetopenid_tiny.gifbin0 -> 3548 bytes
-rw-r--r--samples/RelyingPartyClassicAsp/images/openid_login.gifbin0 -> 237 bytes
-rw-r--r--samples/RelyingPartyClassicAsp/login.asp45
-rw-r--r--samples/RelyingPartyClassicAsp/logout.asp12
-rw-r--r--samples/RelyingPartyClassicAsp/styles.css23
-rw-r--r--samples/RelyingPartyPortal/login.aspx6
-rw-r--r--samples/RelyingPartyPortal/login.aspx.cs10
-rw-r--r--samples/RelyingPartyPortal/login.aspx.designer.cs13
-rw-r--r--src/DotNetOpenId.Test/Extensions/SimpleRegistrationTests.cs38
-rw-r--r--src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs100
-rw-r--r--src/DotNetOpenId.sln37
-rw-r--r--src/DotNetOpenId/DotNetOpenId.csproj5
-rw-r--r--src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs72
-rw-r--r--src/DotNetOpenId/Interop/AuthenticationResponseShim.cs91
-rw-r--r--src/DotNetOpenId/Interop/OpenIdRelyingPartyShim.cs105
-rw-r--r--src/DotNetOpenId/Provider/CheckIdRequest.cs19
-rw-r--r--src/DotNetOpenId/RelyingParty/AssociationPreference.cs36
-rw-r--r--src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs186
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs220
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js549
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdLogin.cs61
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs133
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs25
-rw-r--r--src/DotNetOpenId/Response.cs15
-rw-r--r--src/DotNetOpenId/Strings.Designer.cs9
-rw-r--r--src/DotNetOpenId/Strings.resx5
-rw-r--r--src/DotNetOpenId/UntrustedWebRequest.cs22
-rw-r--r--src/DotNetOpenId/Util.cs29
-rw-r--r--src/DotNetOpenId/Yadis/Yadis.cs5
-rw-r--r--src/specs/OpenIdAjaxTextBox.htm51
-rw-r--r--src/version.txt2
35 files changed, 1630 insertions, 363 deletions
diff --git a/build.proj b/build.proj
index eca301f..c26b41b 100644
--- a/build.proj
+++ b/build.proj
@@ -18,6 +18,7 @@
$(ProjectRoot)\samples\ProviderPortal;
$(ProjectRoot)\samples\RelyingPartyMvc;
$(ProjectRoot)\samples\RelyingPartyPortal;
+ $(ProjectRoot)\samples\RelyingPartyClassicAsp;
" />
<Samples Include="$(ProjectRoot)\samples\**\*.csproj" />
</ItemGroup>
@@ -169,7 +170,7 @@
<Zip Files="@(AllDropTargets)" ZipFileName="$(DropZip)" WorkingDirectory="$(ProjectRoot)\drops" />
</Target>
- <Target Name="Nightly" DependsOnTargets="Drop;Test">
+ <Target Name="Nightly" DependsOnTargets="Drop">
</Target>
diff --git a/doc/logo/dotnetopenid_64x64.png b/doc/logo/dotnetopenid_64x64.png
new file mode 100644
index 0000000..7b11cad
--- /dev/null
+++ b/doc/logo/dotnetopenid_64x64.png
Binary files differ
diff --git a/samples/RelyingPartyClassicAsp/MembersOnly.asp b/samples/RelyingPartyClassicAsp/MembersOnly.asp
new file mode 100644
index 0000000..d146025
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/MembersOnly.asp
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<%
+If Session("ClaimedIdentifier") = "" Then
+ Response.Redirect("login.asp")
+End If
+%>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>DotNetOpenId Classic ASP sample: Members Only area</title>
+ <link href="styles.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+ <div>
+ <a href="http://dotnetopenid.googlecode.com">
+ <img runat="server" src="images/dotnetopenid_tiny.gif" title="Jump to the project web site."
+ alt="DotNetOpenId" border='0' /></a>
+ </div>
+ <h2>
+ Members Only Area
+ </h2>
+ <p>
+ Congratulations, <b><%=Session("ClaimedIdentifier") %></b>.
+ You have completed the OpenID login process.
+ </p>
+ <p><a href="logout.asp">Log out</a>. </p>
+</body>
+</html>
diff --git a/samples/RelyingPartyClassicAsp/default.asp b/samples/RelyingPartyClassicAsp/default.asp
new file mode 100644
index 0000000..83bbe60
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/default.asp
@@ -0,0 +1,39 @@
+<!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>DotNetOpenId Classic ASP sample</title>
+ <link href="styles.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+ <div>
+ <a href="http://dotnetopenid.googlecode.com">
+ <img runat="server" src="images/dotnetopenid_tiny.gif" title="Jump to the project web site."
+ alt="DotNetOpenId" border='0' /></a>
+ </div>
+ <h2>Classic ASP Relying Party</h2>
+ <p>Visit the <a href="MembersOnly.asp">Members Only</a> area. (This will trigger
+ a login demo). </p>
+ <h3>Required steps for this sample to work on your own machine:</h3>
+ <p>Although classic ASP cannot access .NET assemblies directly, it does know how to
+ call COM components.&nbsp; DotNetOpenId exposes a COM server to allow classic ASP
+ and other COM clients to utilize it for easy OpenID support. The DotNetOpenId.dll
+ assembly must be registered as a COM server on each development box and web server
+ in order for COM clients such as classic ASP to find it.</p>
+ <p>To register DotNetOpenId as a COM server, complete these steps.</p>
+ <ol>
+ <li>At an administrator command prompt, navigate to a directory where the DotNetOpenId
+ assembly is found.</li>
+ <li>Register DotNetOpenId as a COM server:<br />
+ <span class="command">%windir%\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe
+ /tlb DotNetOpenId.dll</span></li>
+ <li>Install DotNetOpenId into the GAC.&nbsp; The gacutil.exe tool may be in an SDK
+ directory, which will be in your path if you opened a Visual Studio Command Prompt.<br />
+ <span class="command">gacutil.exe /i DotNetOpenId.dll</span></li>
+ </ol>
+ <p>Another thing to be aware of is that with classic ASP there is no Web.config
+ file in which to customize DotNetOpenId behavior.&nbsp; And the COM interfaces
+ that DotNetOpenId exposes are a very limited subset of full functionality
+ available to .NET clients.&nbsp; Please send feature requests to
+ <a href="mailto:dotnetopenid@googlegroups.com">dotnetopenid@googlegroups.com</a>.</p>
+</body>
+</html>
diff --git a/samples/RelyingPartyClassicAsp/images/dotnetopenid_tiny.gif b/samples/RelyingPartyClassicAsp/images/dotnetopenid_tiny.gif
new file mode 100644
index 0000000..c4ed4f5
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/images/dotnetopenid_tiny.gif
Binary files differ
diff --git a/samples/RelyingPartyClassicAsp/images/openid_login.gif b/samples/RelyingPartyClassicAsp/images/openid_login.gif
new file mode 100644
index 0000000..cde836c
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/images/openid_login.gif
Binary files differ
diff --git a/samples/RelyingPartyClassicAsp/login.asp b/samples/RelyingPartyClassicAsp/login.asp
new file mode 100644
index 0000000..fb37113
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/login.asp
@@ -0,0 +1,45 @@
+<!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>DotNetOpenId Classic ASP sample: Login</title>
+ <link href="styles.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+ <div>
+ <a href="http://dotnetopenid.googlecode.com">
+ <img runat="server" src="images/dotnetopenid_tiny.gif" title="Jump to the project web site."
+ alt="DotNetOpenId" border='0' /></a>
+ </div>
+ <h2>Login Page</h2>
+ <%
+ dim realm, thisPageUrl, requestUrl, dnoi, authentication
+ realm = "http://" + Request.ServerVariables("HTTP_HOST") + "/classicaspdnoi/"
+ thisPageUrl = "http://" + Request.ServerVariables("HTTP_HOST") + Request.ServerVariables("URL")
+ requestUrl = "http://" + Request.ServerVariables("HTTP_HOST") + Request.ServerVariables("HTTP_URL")
+ Set dnoi = server.CreateObject("DotNetOpenId.RelyingParty.OpenIdRelyingParty")
+ Set authentication = dnoi.ProcessAuthentication(requestUrl, Request.Form)
+ if Not authentication Is Nothing then
+ If authentication.Successful Then
+ Session("ClaimedIdentifier") = authentication.ClaimedIdentifier
+ Response.Redirect "MembersOnly.asp"
+ else
+ Response.Write "Authentication failed: " + authentication.ExceptionMessage
+ end if
+ elseif Request.Form("openid_identifier") <> "" then
+ dim redirectUrl
+ redirectUrl = dnoi.CreateRequest(Request.Form("openid_identifier"), realm, thisPageUrl)
+ Response.Redirect redirectUrl
+ End If
+ %>
+ <form action="login.asp" method="post">
+ OpenID Login:
+ <input class="openid" name="openid_identifier" value="<%=Server.HTMLEncode(Request.Form("openid_identifier"))%>" />
+ <input type="submit" value="Login" />
+ </form>
+
+ <script>
+ document.getElementsByName('openid_identifier')[0].focus();
+ </script>
+
+</body>
+</html>
diff --git a/samples/RelyingPartyClassicAsp/logout.asp b/samples/RelyingPartyClassicAsp/logout.asp
new file mode 100644
index 0000000..80e98db
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/logout.asp
@@ -0,0 +1,12 @@
+<!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>DotNetOpenId Classic ASP sample</title>
+</head>
+<body>
+ <%
+ Session.Abandon
+ Response.Redirect "default.asp"
+ %>
+</body>
+</html>
diff --git a/samples/RelyingPartyClassicAsp/styles.css b/samples/RelyingPartyClassicAsp/styles.css
new file mode 100644
index 0000000..8027523
--- /dev/null
+++ b/samples/RelyingPartyClassicAsp/styles.css
@@ -0,0 +1,23 @@
+h2
+{
+ font-style: italic;
+}
+
+body
+{
+ font-family: Cambria, Arial, Times New Roman;
+ font-size: 12pt;
+}
+
+.command
+{
+ font-family: "Courier New", Courier, monospace;
+}
+
+input.openid
+{
+ background-image: url(images/openid_login.gif);
+ background-repeat: no-repeat;
+ background-position: 0 50%;
+ padding-left: 15px;
+}
diff --git a/samples/RelyingPartyPortal/login.aspx b/samples/RelyingPartyPortal/login.aspx
index ba1579a..4c42d03 100644
--- a/samples/RelyingPartyPortal/login.aspx
+++ b/samples/RelyingPartyPortal/login.aspx
@@ -8,7 +8,7 @@
RequestEmail="Request" RequestGender="Require" RequestPostalCode="Require" RequestTimeZone="Require"
RememberMeVisible="True" PolicyUrl="~/PrivacyPolicy.aspx" TabIndex="1"
OnLoggedIn="OpenIdLogin1_LoggedIn" OnLoggingIn="OpenIdLogin1_LoggingIn"
- OnCanceled="OpenIdLogin1_Canceled" OnFailed="OpenIdLogin1_Failed" OnSetupRequired="OpenIdLogin1_SetupRequired" />
+ OnSetupRequired="OpenIdLogin1_SetupRequired" />
<fieldset title="Knobs">
<asp:CheckBox ID="requireSslCheckBox" runat="server"
Text="RequireSsl (high security) mode"
@@ -21,9 +21,7 @@
</asp:CheckBoxList>
</fieldset>
<br />
- <asp:Label ID="loginFailedLabel" runat="server" EnableViewState="False" Text="Login failed"
- Visible="False" />
- <asp:Label ID="loginCanceledLabel" runat="server" EnableViewState="False" Text="Login canceled"
+ <asp:Label ID="setupRequiredLabel" runat="server" EnableViewState="False" Text="You must log into your Provider first to use Immediate mode."
Visible="False" />
<p>
<asp:ImageButton runat="server" ImageUrl="~/images/yahoo.png" ID="yahooLoginButton"
diff --git a/samples/RelyingPartyPortal/login.aspx.cs b/samples/RelyingPartyPortal/login.aspx.cs
index a987a4c..562221f 100644
--- a/samples/RelyingPartyPortal/login.aspx.cs
+++ b/samples/RelyingPartyPortal/login.aspx.cs
@@ -28,16 +28,8 @@ public partial class login : System.Web.UI.Page {
State.ProfileFields = e.Response.GetExtension<ClaimsResponse>();
State.PapePolicies = e.Response.GetExtension<PolicyResponse>();
}
- protected void OpenIdLogin1_Failed(object sender, OpenIdEventArgs e) {
- loginFailedLabel.Visible = true;
- loginFailedLabel.Text += ": " + e.Response.Exception.Message;
- }
- protected void OpenIdLogin1_Canceled(object sender, OpenIdEventArgs e) {
- loginCanceledLabel.Visible = true;
- }
protected void OpenIdLogin1_SetupRequired(object sender, OpenIdEventArgs e) {
- loginFailedLabel.Text = "You must log into your Provider first to use Immediate mode.";
- loginFailedLabel.Visible = true;
+ setupRequiredLabel.Visible = true;
}
protected void yahooLoginButton_Click(object sender, ImageClickEventArgs e) {
diff --git a/samples/RelyingPartyPortal/login.aspx.designer.cs b/samples/RelyingPartyPortal/login.aspx.designer.cs
index 8c888ca..bd305b4 100644
--- a/samples/RelyingPartyPortal/login.aspx.designer.cs
+++ b/samples/RelyingPartyPortal/login.aspx.designer.cs
@@ -49,22 +49,13 @@ public partial class login {
protected global::System.Web.UI.WebControls.CheckBoxList papePolicies;
/// <summary>
- /// loginFailedLabel control.
+ /// setupRequiredLabel control.
/// </summary>
/// <remarks>
/// Auto-generated field.
/// To modify move field declaration from designer file to code-behind file.
/// </remarks>
- protected global::System.Web.UI.WebControls.Label loginFailedLabel;
-
- /// <summary>
- /// loginCanceledLabel control.
- /// </summary>
- /// <remarks>
- /// Auto-generated field.
- /// To modify move field declaration from designer file to code-behind file.
- /// </remarks>
- protected global::System.Web.UI.WebControls.Label loginCanceledLabel;
+ protected global::System.Web.UI.WebControls.Label setupRequiredLabel;
/// <summary>
/// yahooLoginButton control.
diff --git a/src/DotNetOpenId.Test/Extensions/SimpleRegistrationTests.cs b/src/DotNetOpenId.Test/Extensions/SimpleRegistrationTests.cs
index 9437f27..f08a047 100644
--- a/src/DotNetOpenId.Test/Extensions/SimpleRegistrationTests.cs
+++ b/src/DotNetOpenId.Test/Extensions/SimpleRegistrationTests.cs
@@ -1,9 +1,10 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text;
-using NUnit.Framework;
using DotNetOpenId.Extensions.SimpleRegistration;
+using NUnit.Framework;
namespace DotNetOpenId.Test.Extensions {
[TestFixture]
@@ -35,5 +36,40 @@ namespace DotNetOpenId.Test.Extensions {
Assert.IsNull(response.FullName);
Assert.AreEqual("andrewarnott@gmail.com", response.Email);
}
+
+ [Test]
+ public void Birthdates() {
+ var response = new ClaimsResponse();
+ // Verify that they both start out as null
+ Assert.IsNull(response.BirthDateRaw);
+ Assert.IsFalse(response.BirthDate.HasValue);
+
+ // Verify that null can be set.
+ response.BirthDate = null;
+ response.BirthDateRaw = null;
+ Assert.IsNull(response.BirthDateRaw);
+ Assert.IsFalse(response.BirthDate.HasValue);
+
+ // Verify that the strong-typed BirthDate property can be set and that it affects the raw property.
+ response.BirthDate = DateTime.Parse("April 4, 1984");
+ Assert.AreEqual(4, response.BirthDate.Value.Month);
+ Assert.AreEqual("1984-04-04", response.BirthDateRaw);
+
+ // Verify that the raw property can be set with a complete birthdate and that it affects the strong-typed property.
+ response.BirthDateRaw = "1998-05-08";
+ Assert.AreEqual("1998-05-08", response.BirthDateRaw);
+ Assert.AreEqual(DateTime.Parse("May 8, 1998", CultureInfo.InvariantCulture), response.BirthDate);
+
+ // Verify that an partial raw birthdate works, and sets the strong-typed property to null since it cannot be represented.
+ response.BirthDateRaw = "2000-00-00";
+ Assert.AreEqual("2000-00-00", response.BirthDateRaw);
+ Assert.IsFalse(response.BirthDate.HasValue);
+ }
+
+ [Test, ExpectedException(typeof(ArgumentException))]
+ public void InvalidRawBirthdate() {
+ var response = new ClaimsResponse();
+ response.BirthDateRaw = "2008";
+ }
}
}
diff --git a/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs b/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs
index cf9a795..0ec8900 100644
--- a/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs
+++ b/src/DotNetOpenId.Test/RelyingParty/OpenIdRelyingPartyTest.cs
@@ -14,6 +14,41 @@ namespace DotNetOpenId.Test.RelyingParty {
readonly Realm realm = new Realm(TestSupport.GetFullUrl(TestSupport.ConsumerPage).AbsoluteUri);
readonly Uri returnTo = TestSupport.GetFullUrl(TestSupport.ConsumerPage);
Uri simpleNonOpenIdRequest = new Uri("http://localhost/hi");
+ const string multipleEndpointXrds = @"<?xml version='1.0' encoding='UTF-8'?>
+<XRD xmlns='xri://$xrd*($v*2.0)'>
+ <Query>=MultipleEndpoint</Query>
+ <Status cid='verified' code='100' />
+ <ProviderID>=!91F2.8153.F600.AE24</ProviderID>
+ <CanonicalID>=!91F2.8153.F600.AE24</CanonicalID>
+ <Service>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
+ <Type match='null'/>
+ <Path select='true'>(+contact)</Path>
+ <Path match='null'/>
+ <MediaType match='default'/>
+ <URI append='qxri'>http://contact.freexri.com/contact/</URI>
+ </Service>
+ <Service priority='20'>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>http://openid.net/signon/1.0</Type>
+ <Path select='true'>(+login)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='none' priority='2'>http://authn.freexri.com/auth10/</URI>
+ <URI append='none' priority='1'>https://authn.freexri.com/auth10/</URI>
+ </Service>
+ <Service priority='10'>
+ <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
+ <Type select='true'>http://specs.openid.net/auth/2.0/signon</Type>
+ <Path select='true'>(+login)</Path>
+ <Path match='default'/>
+ <MediaType match='default'/>
+ <URI append='none' priority='2'>http://authn.freexri.com/auth20/</URI>
+ <URI append='none' priority='1'>https://authn.freexri.com/auth20/</URI>
+ </Service>
+ <ServedBy>OpenXRI</ServedBy>
+</XRD>";
[SetUp]
public void Setup() {
@@ -86,6 +121,34 @@ namespace DotNetOpenId.Test.RelyingParty {
Assert.AreEqual(0, new Uri(request.ClaimedIdentifier).Fragment.Length);
}
+ /// <summary>
+ /// Verifies that the deferred generation of request objects does not result in
+ /// deferred input validation.
+ /// </summary>
+ [Test, ExpectedException(typeof(ArgumentNullException))]
+ public void CreateRequestsPerformsImmediateValidation() {
+ var consumer = TestSupport.CreateRelyingParty(null);
+ consumer.CreateRequests(null, realm, returnTo);
+ }
+
+ [Test]
+ public void CreateRequestsReturnsEmpty() {
+ MockHttpRequest.RegisterMockResponse(new Uri("http://host/"), "text/html", "<html/>");
+ var consumer = TestSupport.CreateRelyingParty(null);
+ CollectionAssert.IsEmpty(consumer.CreateRequests("http://host/", realm, returnTo));
+ }
+
+ [Test]
+ public void CreateRequestsReturnsMultiple() {
+ MockHttpRequest.RegisterMockXrdsResponses(new Dictionary<string, string> {
+ {"https://xri.net/=MultipleEndpoint?_xrd_r=application/xrd%2Bxml;sep=false", multipleEndpointXrds},
+ });
+ var consumer = TestSupport.CreateRelyingParty(null);
+ var requests = consumer.CreateRequests("=MultipleEndpoint", realm, returnTo);
+ int count = Util.Count(requests);
+ Assert.Greater(count, 1, "Expected more than one auth request to be generated.");
+ }
+
[Test]
public void AssociationCreationWithStore() {
TestSupport.ResetStores(); // get rid of existing associations so a new one is created
@@ -219,43 +282,8 @@ namespace DotNetOpenId.Test.RelyingParty {
[Test]
public void MultipleServiceEndpoints() {
- string xrds = @"<?xml version='1.0' encoding='UTF-8'?>
-<XRD xmlns='xri://$xrd*($v*2.0)'>
- <Query>=MultipleEndpoint</Query>
- <Status cid='verified' code='100' />
- <ProviderID>=!91F2.8153.F600.AE24</ProviderID>
- <CanonicalID>=!91F2.8153.F600.AE24</CanonicalID>
- <Service>
- <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
- <Type select='true'>xri://+i-service*(+contact)*($v*1.0)</Type>
- <Type match='null'/>
- <Path select='true'>(+contact)</Path>
- <Path match='null'/>
- <MediaType match='default'/>
- <URI append='qxri'>http://contact.freexri.com/contact/</URI>
- </Service>
- <Service priority='20'>
- <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
- <Type select='true'>http://openid.net/signon/1.0</Type>
- <Path select='true'>(+login)</Path>
- <Path match='default'/>
- <MediaType match='default'/>
- <URI append='none' priority='2'>http://authn.freexri.com/auth10/</URI>
- <URI append='none' priority='1'>https://authn.freexri.com/auth10/</URI>
- </Service>
- <Service priority='10'>
- <ProviderID>@!7F6F.F50.A4E4.1133</ProviderID>
- <Type select='true'>http://specs.openid.net/auth/2.0/signon</Type>
- <Path select='true'>(+login)</Path>
- <Path match='default'/>
- <MediaType match='default'/>
- <URI append='none' priority='2'>http://authn.freexri.com/auth20/</URI>
- <URI append='none' priority='1'>https://authn.freexri.com/auth20/</URI>
- </Service>
- <ServedBy>OpenXRI</ServedBy>
-</XRD>";
MockHttpRequest.RegisterMockXrdsResponses(new Dictionary<string, string> {
- {"https://xri.net/=MultipleEndpoint?_xrd_r=application/xrd%2Bxml;sep=false", xrds},
+ {"https://xri.net/=MultipleEndpoint?_xrd_r=application/xrd%2Bxml;sep=false", multipleEndpointXrds},
});
OpenIdRelyingParty rp = new OpenIdRelyingParty(null, null, null);
Realm realm = new Realm("http://somerealm");
diff --git a/src/DotNetOpenId.sln b/src/DotNetOpenId.sln
index bfd2752..34408cb 100644
--- a/src/DotNetOpenId.sln
+++ b/src/DotNetOpenId.sln
@@ -43,6 +43,32 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "DotNetOpenId.TestWeb", "Dot
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RelyingPartyMvc", "..\samples\RelyingPartyMvc\RelyingPartyMvc.csproj", "{07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}"
EndProject
+Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "RelyingPartyClassicAsp", "..\samples\RelyingPartyClassicAsp", "{3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}"
+ ProjectSection(WebsiteProperties) = preProject
+ TargetFramework = "2.0"
+ Debug.AspNetCompiler.VirtualPath = "/RelyingPartyClassicAsp"
+ Debug.AspNetCompiler.PhysicalPath = "..\samples\RelyingPartyClassicAsp\"
+ Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\RelyingPartyClassicAsp\"
+ Debug.AspNetCompiler.Updateable = "true"
+ Debug.AspNetCompiler.ForceOverwrite = "true"
+ Debug.AspNetCompiler.FixedNames = "false"
+ Debug.AspNetCompiler.Debug = "True"
+ Release.AspNetCompiler.VirtualPath = "/RelyingPartyClassicAsp"
+ Release.AspNetCompiler.PhysicalPath = "..\samples\RelyingPartyClassicAsp\"
+ Release.AspNetCompiler.TargetPath = "PrecompiledWeb\RelyingPartyClassicAsp\"
+ Release.AspNetCompiler.Updateable = "true"
+ Release.AspNetCompiler.ForceOverwrite = "true"
+ Release.AspNetCompiler.FixedNames = "false"
+ Release.AspNetCompiler.Debug = "False"
+ VWDPort = "61865"
+ StartServerOnDebug = "false"
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Specs", "Specs", "{44A45E8C-5AB9-4D40-A687-81565F2878DB}"
+ ProjectSection(SolutionItems) = preProject
+ specs\OpenIdAjaxTextBox.htm = specs\OpenIdAjaxTextBox.htm
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|.NET = Debug|.NET
@@ -113,6 +139,16 @@ Global
{07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}.Release|Any CPU.Build.0 = Release|Any CPU
{07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{07B193F1-68AD-4E9C-98AF-BEFB5E9403CB}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Debug|.NET.ActiveCfg = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Release|.NET.ActiveCfg = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Release|Any CPU.Build.0 = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Release|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9}.Release|Mixed Platforms.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -121,5 +157,6 @@ Global
{2A59DE0A-B76A-4B42-9A33-04D34548353D} = {48A90678-A754-4E6E-98E2-7C519607C85F}
{51BCD5E9-E17A-4FB2-BAC8-C156DD7A1CA4} = {48A90678-A754-4E6E-98E2-7C519607C85F}
{07B193F1-68AD-4E9C-98AF-BEFB5E9403CB} = {48A90678-A754-4E6E-98E2-7C519607C85F}
+ {3B59898A-B8ED-4F6D-848F-B11CB1B21EB9} = {48A90678-A754-4E6E-98E2-7C519607C85F}
EndGlobalSection
EndGlobal
diff --git a/src/DotNetOpenId/DotNetOpenId.csproj b/src/DotNetOpenId/DotNetOpenId.csproj
index d68b245..3587133 100644
--- a/src/DotNetOpenId/DotNetOpenId.csproj
+++ b/src/DotNetOpenId/DotNetOpenId.csproj
@@ -66,7 +66,10 @@
<Compile Include="DiffieHellmanUtil.cs" />
<Compile Include="Extensions\IClientScriptExtensionResponse.cs" />
<Compile Include="Extensions\ExtensionManager.cs" />
+ <Compile Include="Interop\AuthenticationResponseShim.cs" />
+ <Compile Include="Interop\OpenIdRelyingPartyShim.cs" />
<Compile Include="Provider\ProviderSecuritySettings.cs" />
+ <Compile Include="RelyingParty\AssociationPreference.cs" />
<Compile Include="RelyingParty\AuthenticationResponseSnapshot.cs" />
<Compile Include="RelyingParty\OpenIdAjaxTextBox.cs" />
<Compile Include="Configuration\ProviderSecuritySettingsElement.cs" />
@@ -230,4 +233,4 @@
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\tools\DotNetOpenId.Versioning.targets" />
-</Project>
+</Project> \ No newline at end of file
diff --git a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
index a9b0cbd..9714a58 100644
--- a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
+++ b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
@@ -7,9 +7,11 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Mail;
using System.Text;
+using System.Text.RegularExpressions;
using System.Xml.Serialization;
using DotNetOpenId.RelyingParty;
@@ -20,9 +22,17 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
/// A struct storing Simple Registration field values describing an
/// authenticating user.
/// </summary>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals"), Serializable()]
+ [SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals"), Serializable()]
public sealed class ClaimsResponse : IExtensionResponse, IClientScriptExtensionResponse
{
+ /// <summary>
+ /// Storage for the raw string birthdate value.
+ /// </summary>
+ private string birthDateRaw;
+ private DateTime? birthDate;
+
+ private static readonly Regex birthDateValidator = new Regex(@"^\d\d\d\d-\d\d-\d\d$");
+
string typeUriToUse;
/// <summary>
@@ -66,7 +76,49 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
/// <summary>
/// The user's birthdate.
/// </summary>
- public DateTime? BirthDate { get; set; }
+ public DateTime? BirthDate {
+ get { return this.birthDate; }
+ set {
+ this.birthDate = value;
+ // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors.
+ if (value.HasValue) {
+ this.birthDateRaw = value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
+ } else {
+ this.birthDateRaw = null;
+ }
+ }
+ }
+ /// <summary>
+ /// The string value of the birthdate. Useful for getting/setting when month and/or day information is not included in the date
+ /// thus preventing use of <see cref="DateTime"/>.
+ /// </summary>
+ /// <value>May be null, or exactly 10 characters in length in the form yyyy-MM-dd.</value>
+ /// <remarks>Setting this property </remarks>
+ public string BirthDateRaw {
+ get { return this.birthDateRaw; }
+ set {
+ if (value != null) {
+ if (!birthDateValidator.IsMatch(value)) {
+ throw new ArgumentException(Strings.SregInvalidBirthdate, "value");
+ }
+ // Update the BirthDate property, if possible.
+ // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors.
+ // Some valid sreg dob values like "2000-00-00" will not work as a DateTime struct,
+ // in which case we null it out, but don't show any error.
+ DateTime newBirthDate;
+ if (DateTime.TryParse(value, out newBirthDate)) {
+ this.birthDate = newBirthDate;
+ } else {
+ Logger.InfoFormat("Simple Registration birthdate '{0}' could not be parsed into a DateTime and may not include month and/or day information. Setting BirthDate property to null.", value);
+ this.birthDate = null;
+ }
+ } else {
+ this.birthDate = null;
+ }
+
+ this.birthDateRaw = value;
+ }
+ }
/// <summary>
/// The gender of the user.
/// </summary>
@@ -126,12 +178,12 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
/// Adds the values of this struct to an authentication response being prepared
/// by an OpenID Provider.
/// </summary>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
IDictionary<string, string> IExtensionResponse.Serialize(Provider.IRequest authenticationRequest) {
if (authenticationRequest == null) throw new ArgumentNullException("authenticationRequest");
Dictionary<string, string> fields = new Dictionary<string, string>();
- if (BirthDate != null) {
- fields.Add(Constants.dob, BirthDate.ToString());
+ if (BirthDateRaw != null) {
+ fields.Add(Constants.dob, BirthDateRaw);
}
if (!String.IsNullOrEmpty(Country)) {
fields.Add(Constants.country, Country);
@@ -176,9 +228,11 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
sreg.TryGetValue(Constants.fullname, out fullName);
FullName = fullName;
if (sreg.TryGetValue(Constants.dob, out dob)) {
- DateTime bd;
- if (DateTime.TryParse(dob, out bd))
- BirthDate = bd;
+ if (dob.Length == 10 && birthDateValidator.IsMatch(dob)) {
+ this.BirthDateRaw = dob;
+ } else {
+ Logger.ErrorFormat("Simple Registration response included invalid value for openid.sreg.dob: {0}", dob);
+ }
}
if (sreg.TryGetValue(Constants.gender, out genderString)) {
switch (genderString) {
@@ -269,7 +323,7 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
if (other == null) return false;
return
- safeEquals(this.BirthDate, other.BirthDate) &&
+ safeEquals(this.BirthDateRaw, other.BirthDateRaw) &&
safeEquals(this.Country, other.Country) &&
safeEquals(this.Language, other.Language) &&
safeEquals(this.Email, other.Email) &&
diff --git a/src/DotNetOpenId/Interop/AuthenticationResponseShim.cs b/src/DotNetOpenId/Interop/AuthenticationResponseShim.cs
new file mode 100644
index 0000000..7a704b7
--- /dev/null
+++ b/src/DotNetOpenId/Interop/AuthenticationResponseShim.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Web;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId.Interop {
+ /// <summary>
+ /// The COM type used to provide details of an authentication result to a relying party COM client.
+ /// </summary>
+ [SuppressMessage("Microsoft.Interoperability", "CA1409:ComVisibleTypesShouldBeCreatable")]
+ [ComVisible(true), Obsolete("This class acts as a COM Server and should not be called directly from .NET code.")]
+ public class AuthenticationResponseShim {
+ private readonly IAuthenticationResponse response;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AuthenticationResponseShim"/> class.
+ /// </summary>
+ /// <param name="response">The response.</param>
+ internal AuthenticationResponseShim(IAuthenticationResponse response) {
+ if (response == null) throw new ArgumentNullException("response");
+ this.response = response;
+ }
+
+ /// <summary>
+ /// An Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
+ public string ClaimedIdentifier {
+ get { return this.response.ClaimedIdentifier; }
+ }
+
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ public string FriendlyIdentifierForDisplay {
+ get { return this.response.FriendlyIdentifierForDisplay; }
+ }
+
+ /// <summary>
+ /// A value indicating whether the authentication attempt succeeded.
+ /// </summary>
+ public bool Successful {
+ get { return this.response.Status == AuthenticationStatus.Authenticated; }
+ }
+
+ /// <summary>
+ /// Details regarding a failed authentication attempt, if available.
+ /// </summary>
+ public string ExceptionMessage {
+ get { return this.response.Exception != null ? this.response.Exception.Message : null; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Interop/OpenIdRelyingPartyShim.cs b/src/DotNetOpenId/Interop/OpenIdRelyingPartyShim.cs
new file mode 100644
index 0000000..7aef5b1
--- /dev/null
+++ b/src/DotNetOpenId/Interop/OpenIdRelyingPartyShim.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Web;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId.Interop {
+ /// <summary>
+ /// The COM interface describing the DotNetOpenId functionality available to
+ /// COM client relying parties.
+ /// </summary>
+ [Guid("00462F34-21BE-456c-B986-B6DDE4DC5CA8")]
+ [InterfaceType(ComInterfaceType.InterfaceIsDual)]
+ public interface IOpenIdRelyingParty {
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given 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>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="OpenIdException">Thrown if no OpenID endpoint could be found.</exception>
+ string CreateRequest(string userSuppliedIdentifier, string realm, string returnToUrl);
+
+ /// <summary>
+ /// Gets the result of a user agent's visit to his OpenId provider in an
+ /// authentication attempt. Null if no response is available.
+ /// </summary>
+ /// <param name="url">The incoming request URL .</param>
+ /// <param name="form">The form data that may have been included in the case of a POST request.</param>
+ /// <returns>The Provider's response to a previous authentication request, or null if no response is present.</returns>
+#pragma warning disable 0618 // we're using the COM type properly
+ AuthenticationResponseShim ProcessAuthentication(string url, string form);
+#pragma warning restore 0618
+ }
+
+ /// <summary>
+ /// Implementation of <see cref="IOpenIdRelyingParty"/>, providing a subset of the
+ /// functionality available to .NET clients.
+ /// </summary>
+ [Guid("4D6FB236-1D66-4311-B761-972C12BB85E8")]
+ [ProgId("DotNetOpenId.RelyingParty.OpenIdRelyingParty")]
+ [ComVisible(true), Obsolete("This class acts as a COM Server and should not be called directly from .NET code.", true)]
+ [ClassInterface(ClassInterfaceType.None)]
+ [ComSourceInterfaces(typeof(IOpenIdRelyingParty))]
+ public class OpenIdRelyingPartyShim : IOpenIdRelyingParty {
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given 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>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="OpenIdException">Thrown if no OpenID endpoint could be found.</exception>
+ [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
+ public string CreateRequest(string userSuppliedIdentifier, string realm, string returnToUrl) {
+ OpenIdRelyingParty rp = new OpenIdRelyingParty(null, null, null);
+ Response response = (Response)rp.CreateRequest(userSuppliedIdentifier, realm, new Uri(returnToUrl)).RedirectingResponse;
+ return response.IndirectMessageAsRequestUri.AbsoluteUri;
+ }
+
+ /// <summary>
+ /// Gets the result of a user agent's visit to his OpenId provider in an
+ /// authentication attempt. Null if no response is available.
+ /// </summary>
+ /// <param name="url">The incoming request URL .</param>
+ /// <param name="form">The form data that may have been included in the case of a POST request.</param>
+ /// <returns>The Provider's response to a previous authentication request, or null if no response is present.</returns>
+ public AuthenticationResponseShim ProcessAuthentication(string url, string form) {
+ Uri uri = new Uri(url);
+ OpenIdRelyingParty rp = new OpenIdRelyingParty(null, uri, HttpUtility.ParseQueryString(uri.Query));
+ if (rp.Response != null) {
+ return new AuthenticationResponseShim(rp.Response);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Provider/CheckIdRequest.cs b/src/DotNetOpenId/Provider/CheckIdRequest.cs
index 373ba2e..fa17200 100644
--- a/src/DotNetOpenId/Provider/CheckIdRequest.cs
+++ b/src/DotNetOpenId/Provider/CheckIdRequest.cs
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Globalization;
using System.Web;
@@ -194,24 +195,8 @@ namespace DotNetOpenId.Provider {
(!IsAuthenticated.Value || !IsDirectedIdentity || (LocalIdentifier != null && ClaimedIdentifier != null));
}
}
- /// <summary>
- /// Get the URL to cancel this request.
- /// </summary>
- internal Uri CancelUrl {
- get {
- if (Immediate)
- throw new InvalidOperationException("Cancel is not an appropriate response to immediate mode requests.");
-
- UriBuilder builder = new UriBuilder(ReturnTo);
- var args = new Dictionary<string, string>();
- args.Add(Protocol.openid.mode, Protocol.Args.Mode.cancel);
- UriUtil.AppendQueryArgs(builder, args);
-
- return builder.Uri;
- }
- }
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily")]
+ [SuppressMessage("Microsoft.Performance", "CA1805:DoNotInitializeUnnecessarily")]
internal CheckIdRequest(OpenIdProvider provider) : base(provider) {
// handle the mandatory protocol fields
string mode = Util.GetRequiredArg(Query, Protocol.openid.mode);
diff --git a/src/DotNetOpenId/RelyingParty/AssociationPreference.cs b/src/DotNetOpenId/RelyingParty/AssociationPreference.cs
new file mode 100644
index 0000000..348c059
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/AssociationPreference.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.RelyingParty {
+ /// <summary>
+ /// Preferences regarding creation and use of an association between a relying party
+ /// and provider for authentication.
+ /// </summary>
+ internal enum AssociationPreference {
+ /// <summary>
+ /// Indicates that an association should be created for use in authentication
+ /// if one has not already been established between the relying party and the
+ /// selected provider.
+ /// </summary>
+ /// <remarks>
+ /// Even with this value, if an association attempt fails or the relying party
+ /// has no application store to recall associations, the authentication may
+ /// proceed without an association.
+ /// </remarks>
+ IfPossible,
+
+ /// <summary>
+ /// Indicates that an association should be used for authentication only if
+ /// it happens to already exist.
+ /// </summary>
+ IfAlreadyEstablished,
+
+ /// <summary>
+ /// Indicates that an authentication attempt should NOT use an OpenID association
+ /// between the relying party and the provider, even if an association was previously
+ /// created.
+ /// </summary>
+ Never,
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
index a3c71ca..a290149 100644
--- a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
@@ -27,19 +27,18 @@ namespace DotNetOpenId.RelyingParty {
[DebuggerDisplay("ClaimedIdentifier: {ClaimedIdentifier}, Mode: {Mode}, OpenId: {protocol.Version}")]
class AuthenticationRequest : IAuthenticationRequest {
- Association assoc;
+ internal AssociationPreference associationPreference = AssociationPreference.IfPossible;
ServiceEndpoint endpoint;
Protocol protocol { get { return endpoint.Protocol; } }
internal OpenIdRelyingParty RelyingParty;
- AuthenticationRequest(string token, Association assoc, ServiceEndpoint endpoint,
+ AuthenticationRequest(ServiceEndpoint endpoint,
Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) {
if (endpoint == null) throw new ArgumentNullException("endpoint");
if (realm == null) throw new ArgumentNullException("realm");
if (returnToUrl == null) throw new ArgumentNullException("returnToUrl");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
- this.assoc = assoc;
this.endpoint = endpoint;
RelyingParty = relyingParty;
Realm = realm;
@@ -48,11 +47,15 @@ namespace DotNetOpenId.RelyingParty {
Mode = AuthenticationRequestMode.Setup;
OutgoingExtensions = ExtensionArgumentsManager.CreateOutgoingExtensions(endpoint.Protocol);
ReturnToArgs = new Dictionary<string, string>();
- if (token != null)
- AddCallbackArguments(DotNetOpenId.RelyingParty.Token.TokenKey, token);
}
- internal static AuthenticationRequest Create(Identifier userSuppliedIdentifier,
- OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl) {
+
+ /// <summary>
+ /// Performs identifier discovery and creates associations and generates authentication requests
+ /// on-demand for as long as new ones can be generated based on the results of Identifier discovery.
+ /// </summary>
+ internal static IEnumerable<AuthenticationRequest> Create(Identifier userSuppliedIdentifier,
+ OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) {
+ // We have a long data validation and preparation process
if (userSuppliedIdentifier == null) throw new ArgumentNullException("userSuppliedIdentifier");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (realm == null) throw new ArgumentNullException("realm");
@@ -79,11 +82,6 @@ namespace DotNetOpenId.RelyingParty {
}
}
- var endpoints = new List<ServiceEndpoint>(userSuppliedIdentifier.Discover());
- ServiceEndpoint endpoint = selectEndpoint(endpoints.AsReadOnly(), relyingParty);
- if (endpoint == null)
- throw new OpenIdException(Strings.OpenIdEndpointNotFound);
-
// Throw an exception now if the realm and the return_to URLs don't match
// as required by the provider. We could wait for the provider to test this and
// fail, but this will be faster and give us a better error message.
@@ -91,19 +89,88 @@ namespace DotNetOpenId.RelyingParty {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.ReturnToNotUnderRealm, returnToUrl, realm));
- string token = new Token(endpoint).Serialize(relyingParty.Store);
- // Retrieve the association, but don't create one, as a creation was already
- // attempted by the selectEndpoint method.
- Association association = relyingParty.Store != null ? getAssociation(relyingParty, endpoint, false) : null;
+ // Perform discovery right now (not deferred).
+ var serviceEndpoints = userSuppliedIdentifier.Discover();
+
+ // Call another method that defers request generation.
+ return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded);
+ }
+
+ /// <summary>
+ /// Performs request generation for the <see cref="Create"/> method.
+ /// All data validation and cleansing steps must have ALREADY taken place.
+ /// </summary>
+ private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier,
+ OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl,
+ IEnumerable<ServiceEndpoint> serviceEndpoints, bool createNewAssociationsAsNeeded) {
+ Logger.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier);
+ IEnumerable<ServiceEndpoint> endpoints = filterAndSortEndpoints(serviceEndpoints, relyingParty);
+
+ // Maintain a list of endpoints that we could not form an association with.
+ // We'll fallback to generating requests to these if the ones we CAN create
+ // an association with run out.
+ var failedAssociationEndpoints = new List<ServiceEndpoint>(0);
+
+ foreach (var endpoint in endpoints) {
+ Logger.InfoFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
+ Logger.DebugFormat("Realm: {0}", realm);
+ Logger.DebugFormat("Return To: {0}", returnToUrl);
+
+ // The strategy here is to prefer endpoints with whom we can create associations.
+ Association association = null;
+ if (relyingParty.Store != null) {
+ // In some scenarios (like the AJAX control wanting ALL auth requests possible),
+ // we don't want to create associations with every Provider. But we'll use
+ // associations where they are already formed from previous authentications.
+ association = getAssociation(relyingParty, endpoint, createNewAssociationsAsNeeded);
+ if (association == null && createNewAssociationsAsNeeded) {
+ Logger.WarnFormat("Failed to create association with {0}. Skipping to next endpoint.", endpoint.ProviderEndpoint);
+ // No association could be created. Add it to the list of failed association
+ // endpoints and skip to the next available endpoint.
+ failedAssociationEndpoints.Add(endpoint);
+ continue;
+ }
+ }
+
+ yield return new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty);
+ }
- return new AuthenticationRequest(
- token, association, endpoint, realm, returnToUrl, relyingParty);
+ // Now that we've run out of endpoints that respond to association requests,
+ // since we apparently are still running, the caller must want another request.
+ // We'll go ahead and generate the requests to OPs that may be down.
+ if (failedAssociationEndpoints.Count > 0) {
+ Logger.WarnFormat("Now generating requests for Provider endpoints that failed initial association attempts.");
+
+ foreach (var endpoint in failedAssociationEndpoints) {
+ Logger.WarnFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
+ Logger.DebugFormat("Realm: {0}", realm);
+ Logger.DebugFormat("Return To: {0}", returnToUrl);
+
+ // Create the auth request, but prevent it from attempting to create an association
+ // because we've already tried. Let's not have it waste time trying again.
+ var authRequest = new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty);
+ authRequest.associationPreference = AssociationPreference.IfAlreadyEstablished;
+ yield return authRequest;
+ }
+ }
+ }
+
+ internal static AuthenticationRequest CreateSingle(Identifier userSuppliedIdentifier,
+ OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl) {
+
+ // Just return the first generated request.
+ var requests = Create(userSuppliedIdentifier, relyingParty, realm, returnToUrl, true).GetEnumerator();
+ if (requests.MoveNext()) {
+ return requests.Current;
+ } else {
+ throw new OpenIdException(Strings.OpenIdEndpointNotFound);
+ }
}
/// <summary>
/// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
/// </summary>
- private static List<ServiceEndpoint> filterAndSortEndpoints(ReadOnlyCollection<ServiceEndpoint> endpoints,
+ private static List<ServiceEndpoint> filterAndSortEndpoints(IEnumerable<ServiceEndpoint> endpoints,
OpenIdRelyingParty relyingParty) {
if (endpoints == null) throw new ArgumentNullException("endpoints");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
@@ -112,10 +179,13 @@ namespace DotNetOpenId.RelyingParty {
EndpointSelector versionFilter = ep => ((ServiceEndpoint)ep).Protocol.Version >= Protocol.Lookup(relyingParty.Settings.MinimumRequiredOpenIdVersion).Version;
EndpointSelector hostingSiteFilter = relyingParty.EndpointFilter ?? (ep => true);
- var filteredEndpoints = new List<IXrdsProviderEndpoint>(endpoints.Count);
+ bool anyFilteredOut = false;
+ var filteredEndpoints = new List<IXrdsProviderEndpoint>();
foreach (ServiceEndpoint endpoint in endpoints) {
if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) {
filteredEndpoints.Add(endpoint);
+ } else {
+ anyFilteredOut = true;
}
}
@@ -126,22 +196,12 @@ namespace DotNetOpenId.RelyingParty {
foreach (ServiceEndpoint endpoint in filteredEndpoints) {
endpointList.Add(endpoint);
}
- return endpointList;
- }
- /// <summary>
- /// Chooses which provider endpoint is the best one to use.
- /// </summary>
- /// <returns>The best endpoint, or null if no acceptable endpoints were found.</returns>
- private static ServiceEndpoint selectEndpoint(ReadOnlyCollection<ServiceEndpoint> endpoints,
- OpenIdRelyingParty relyingParty) {
-
- List<ServiceEndpoint> filteredEndpoints = filterAndSortEndpoints(endpoints, relyingParty);
- if (filteredEndpoints.Count != endpoints.Count) {
+ if (anyFilteredOut) {
Logger.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count);
}
if (Logger.IsDebugEnabled) {
- if (Util.AreSequencesEquivalent(endpoints, filteredEndpoints)) {
+ if (Util.AreSequencesEquivalent(endpoints, endpointList)) {
Logger.Debug("Filtering and sorting of endpoints did not affect the list.");
} else {
Logger.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
@@ -149,44 +209,18 @@ namespace DotNetOpenId.RelyingParty {
}
}
- // If there are no endpoint candidates...
- if (filteredEndpoints.Count == 0) {
- return null;
- }
-
- // If we don't have an application store, we have no place to record an association to
- // and therefore can only take our best shot at one of the endpoints.
- if (relyingParty.Store == null) {
- Logger.Debug("No state store, so the first endpoint available is selected.");
- return filteredEndpoints[0];
- }
-
- // Go through each endpoint until we find one that we can successfully create
- // an association with. This is our only hint about whether an OP is up and running.
- // The idea here is that we don't want to redirect the user to a dead OP for authentication.
- // If the user has multiple OPs listed in his/her XRDS document, then we'll go down the list
- // and try each one until we find one that's good.
- int winningEndpointIndex = 0;
- foreach (ServiceEndpoint endpointCandidate in filteredEndpoints) {
- winningEndpointIndex++;
- // One weakness of this method is that an OP that's down, but with whom we already
- // created an association in the past will still pass this "are you alive?" test.
- Association association = getAssociation(relyingParty, endpointCandidate, true);
- if (association != null) {
- Logger.DebugFormat("Endpoint #{0} (1-based index) responded to an association request. Selecting that endpoint.", winningEndpointIndex);
- // We have a winner!
- return endpointCandidate;
- }
- }
-
- // Since all OPs failed to form an association with us, just return the first endpoint
- // and hope for the best.
- Logger.Debug("All endpoints failed to respond to an association request. Selecting first endpoint to try to authenticate to.");
- return endpoints[0];
+ return endpointList;
}
+
static Association getAssociation(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, bool createNewAssociationIfNeeded) {
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (provider == null) throw new ArgumentNullException("provider");
+
+ // If the RP has no application store for associations, there's no point in creating one.
+ if (relyingParty.Store == null) {
+ return null;
+ }
+
// TODO: we need a way to lookup an association that fulfills a given set of security
// requirements. We may have a SHA-1 association and a SHA-256 association that need
// to be called for specifically. (a bizzare scenario, admittedly, making this low priority).
@@ -282,6 +316,13 @@ namespace DotNetOpenId.RelyingParty {
UriBuilder returnToBuilder = new UriBuilder(ReturnToUrl);
UriUtil.AppendAndReplaceQueryArgs(returnToBuilder, this.ReturnToArgs);
+ string token = new Token(endpoint).Serialize(this.RelyingParty.Store);
+ if (token != null) {
+ UriUtil.AppendQueryArgs(returnToBuilder, new Dictionary<string, string> {
+ { DotNetOpenId.RelyingParty.Token.TokenKey, token },
+ });
+ }
+
var qsArgs = new Dictionary<string, string>();
qsArgs.Add(protocol.openid.mode, (Mode == AuthenticationRequestMode.Immediate) ?
@@ -295,8 +336,17 @@ namespace DotNetOpenId.RelyingParty {
qsArgs.Add(protocol.openid.Realm, Realm);
qsArgs.Add(protocol.openid.return_to, returnToBuilder.Uri.AbsoluteUri);
- if (this.assoc != null)
- qsArgs.Add(protocol.openid.assoc_handle, this.assoc.Handle);
+ Association association = null;
+ if (associationPreference != AssociationPreference.Never) {
+ association = getAssociation(RelyingParty, endpoint, associationPreference == AssociationPreference.IfPossible);
+ if (association != null) {
+ qsArgs.Add(protocol.openid.assoc_handle, association.Handle);
+ } else {
+ // Avoid trying to create the association again if the redirecting response
+ // is generated again.
+ associationPreference = AssociationPreference.IfAlreadyEstablished;
+ }
+ }
// Add on extension arguments
foreach (var pair in OutgoingExtensions.GetArgumentsToSend(true))
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs
index 4c7f145..d61a6b7 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
@@ -24,7 +25,7 @@ namespace DotNetOpenId.RelyingParty {
/// </summary>
[DefaultProperty("Text"), ValidationProperty("Text")]
[ToolboxData("<{0}:OpenIdAjaxTextBox runat=\"server\" />")]
- public class OpenIdAjaxTextBox : WebControl {
+ public class OpenIdAjaxTextBox : WebControl, ICallbackEventHandler {
internal const string EmbeddedScriptResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.OpenIdAjaxTextBox.js";
internal const string EmbeddedDotNetOpenIdLogoResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.dotnetopenid_16x16.gif";
internal const string EmbeddedSpinnerResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.spinner.gif";
@@ -35,6 +36,7 @@ namespace DotNetOpenId.RelyingParty {
const string authenticationResponseViewStateKey = "AuthenticationResponse";
const string authDataViewStateKey = "AuthData";
+ string openidAuthDataFormKey { get { return Name + "_openidAuthData"; } }
IAuthenticationResponse authenticationResponse;
/// <summary>
/// Gets the completed authentication response.
@@ -47,13 +49,13 @@ namespace DotNetOpenId.RelyingParty {
// from viewstate and return that.
IAuthenticationResponse viewstateResponse = ViewState[authenticationResponseViewStateKey] as IAuthenticationResponse;
string viewstateAuthData = ViewState[authDataViewStateKey] as string;
- string formAuthData = Page.Request.Form["openidAuthData"];
+ string formAuthData = Page.Request.Form[openidAuthDataFormKey];
// First see if there is fresh auth data to be processed into a response.
- if (formAuthData != null && !string.Equals(viewstateAuthData, formAuthData, StringComparison.Ordinal)) {
+ if (!string.IsNullOrEmpty(formAuthData) && !string.Equals(viewstateAuthData, formAuthData, StringComparison.Ordinal)) {
ViewState[authDataViewStateKey] = formAuthData;
- Uri authUri = new Uri(formAuthData ?? viewstateAuthData);
+ Uri authUri = new Uri(formAuthData);
var authDataFields = HttpUtility.ParseQueryString(authUri.Query);
var rp = new OpenIdRelyingParty(OpenIdRelyingParty.HttpApplicationStore,
authUri, authDataFields);
@@ -128,11 +130,20 @@ namespace DotNetOpenId.RelyingParty {
}
const string timeoutViewStateKey = "Timeout";
- readonly TimeSpan timeoutDefault = TimeSpan.FromSeconds(8);
+ TimeSpan timeoutDefault {
+ get {
+ if (Debugger.IsAttached) {
+ Logger.Warn("Debugger is attached. Inflating default OpenIdAjaxTextbox.Timeout value to infinity.");
+ return TimeSpan.MaxValue;
+ } else {
+ return TimeSpan.FromSeconds(8);
+ }
+ }
+ }
/// <summary>
/// Gets/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("Behavior")]
+ [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:01"), Category("Behavior")]
[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 { return (TimeSpan)(ViewState[timeoutViewStateKey] ?? timeoutDefault); }
@@ -142,6 +153,21 @@ namespace DotNetOpenId.RelyingParty {
}
}
+ const string throttleViewStateKey = "Throttle";
+ const int throttleDefault = 3;
+ /// <summary>
+ /// Gets/sets the maximum number of OpenID Providers to simultaneously try to authenticate with.
+ /// </summary>
+ [Browsable(true), DefaultValue(throttleDefault), Category("Behavior")]
+ [Description("The maximum number of OpenID Providers to simultaneously try to authenticate with.")]
+ public int Throttle {
+ get { return (int)(ViewState[throttleViewStateKey] ?? throttleDefault); }
+ set {
+ if (value <= 0) throw new ArgumentOutOfRangeException("value");
+ ViewState[throttleViewStateKey] = value;
+ }
+ }
+
const string logonTextViewStateKey = "LoginText";
const string logonTextDefault = "LOG IN";
/// <summary>
@@ -199,7 +225,7 @@ namespace DotNetOpenId.RelyingParty {
}
const string authenticationSucceededToolTipViewStateKey = "AuthenticationSucceededToolTip";
- const string authenticationSucceededToolTipDefault = "Authenticated.";
+ const string authenticationSucceededToolTipDefault = "Authenticated by {0}.";
/// <summary>
/// Gets/sets the tool tip text that appears when authentication succeeds.
/// </summary>
@@ -210,6 +236,18 @@ namespace DotNetOpenId.RelyingParty {
set { ViewState[authenticationSucceededToolTipViewStateKey] = value ?? string.Empty; }
}
+ const string authenticatedAsToolTipViewStateKey = "AuthenticatedAsToolTip";
+ const string authenticatedAsToolTipDefault = "Authenticated as {0}.";
+ /// <summary>
+ /// Gets/sets the tool tip text that appears on the green checkmark when authentication succeeds.
+ /// </summary>
+ [Bindable(true), DefaultValue(authenticatedAsToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears on the green checkmark when authentication succeeds.")]
+ public string AuthenticatedAsToolTip {
+ get { return (string)(ViewState[authenticatedAsToolTipViewStateKey] ?? authenticatedAsToolTipDefault); }
+ set { ViewState[authenticatedAsToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
const string authenticationFailedToolTipViewStateKey = "AuthenticationFailedToolTip";
const string authenticationFailedToolTipDefault = "Authentication failed.";
/// <summary>
@@ -554,19 +592,20 @@ namespace DotNetOpenId.RelyingParty {
} else {
NameValueCollection query = Util.GetQueryOrFormFromContextNVC();
string userSuppliedIdentifier = query["dotnetopenid.userSuppliedIdentifier"];
- if (!string.IsNullOrEmpty(userSuppliedIdentifier)) {
- Logger.Info("AJAX (iframe) request detected.");
- if (query["dotnetopenid.phase"] == "2") {
- OnUnconfirmedPositiveAssertion();
- reportDiscoveryResult();
- } else {
- performDiscovery(userSuppliedIdentifier);
- }
+ if (!string.IsNullOrEmpty(userSuppliedIdentifier) && query["dotnetopenid.phase"] == "2") {
+ reportAuthenticationResult();
}
}
}
private void prepareClientJavascript() {
+ string identifierParameterName = "identifier";
+ string discoveryCallbackResultParameterName = "resultFunction";
+ string discoveryErrorCallbackParameterName = "errorCallback";
+ string discoveryCallback = Page.ClientScript.GetCallbackEventReference(
+ this, identifierParameterName, discoveryCallbackResultParameterName,
+ identifierParameterName, discoveryErrorCallbackParameterName, true);
+
// Import the .js file where most of the code is.
Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdAjaxTextBox), EmbeddedScriptResourceName);
// Call into the .js file with initialization information.
@@ -577,12 +616,13 @@ namespace DotNetOpenId.RelyingParty {
startupScript.AppendLine("box.focus();");
}
startupScript.AppendFormat(CultureInfo.InvariantCulture,
- "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15});{16}",
+ "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, function({18}, {19}, {20}) {{{21}}});{22}",
Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), OpenIdTextBox.EmbeddedLogoResourceName)),
Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedDotNetOpenIdLogoResourceName)),
Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedSpinnerResourceName)),
Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedLoginSuccessResourceName)),
Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedLoginFailureResourceName)),
+ Throttle,
Timeout.TotalMilliseconds,
string.IsNullOrEmpty(OnClientAssertionReceived) ? "null" : "'" + OnClientAssertionReceived.Replace(@"\", @"\\").Replace("'", @"\'") + "'",
Util.GetSafeJavascriptValue(LogOnText),
@@ -593,12 +633,14 @@ namespace DotNetOpenId.RelyingParty {
Util.GetSafeJavascriptValue(IdentifierRequiredMessage),
Util.GetSafeJavascriptValue(LogOnInProgressMessage),
Util.GetSafeJavascriptValue(AuthenticationSucceededToolTip),
+ Util.GetSafeJavascriptValue(AuthenticatedAsToolTip),
Util.GetSafeJavascriptValue(AuthenticationFailedToolTip),
+ identifierParameterName,
+ discoveryCallbackResultParameterName,
+ discoveryErrorCallbackParameterName,
+ discoveryCallback,
Environment.NewLine);
- if (AuthenticationResponse != null && AuthenticationResponse.Status == AuthenticationStatus.Authenticated) {
- startupScript.AppendFormat("box.dnoi_internal.openidAuthResult('{0}');{1}", ViewState[authDataViewStateKey].ToString().Replace("'", "\\'"), Environment.NewLine);
- }
startupScript.AppendLine("</script>");
Page.ClientScript.RegisterStartupScript(GetType(), "ajaxstartup", startupScript.ToString());
@@ -608,8 +650,8 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
", Name));
}
- private IAuthenticationRequest createRequest(Identifier userSuppliedIdentifier) {
- IAuthenticationRequest request;
+ private List<IAuthenticationRequest> createRequests(string userSuppliedIdentifier, bool immediate) {
+ var requests = new List<IAuthenticationRequest>();
OpenIdRelyingParty rp = new OpenIdRelyingParty();
@@ -629,39 +671,56 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
// might slip through our validator control if it is disabled.
Realm typedRealm = new Realm(realm);
if (string.IsNullOrEmpty(ReturnToUrl)) {
- request = rp.CreateRequest(userSuppliedIdentifier, typedRealm);
+ requests.AddRange(rp.CreateRequests(userSuppliedIdentifier, typedRealm));
} else {
// Since the user actually gave us a return_to value,
// the "approximation" is exactly what we want.
- request = rp.CreateRequest(userSuppliedIdentifier, typedRealm, returnToApproximation);
+ requests.AddRange(rp.CreateRequests(userSuppliedIdentifier, typedRealm, returnToApproximation));
}
- return request;
- }
+ // 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 = RemoveDuplicateEndpoints(requests);
- private void performDiscovery(string userSuppliedIdentifier) {
- if (String.IsNullOrEmpty(userSuppliedIdentifier)) throw new ArgumentNullException("userSuppliedIdentifier");
+ // Configure each generated request.
NameValueCollection query = Util.GetQueryOrFormFromContextNVC();
- Logger.InfoFormat("Discovery on {0} requested.", userSuppliedIdentifier);
-
- try {
- IAuthenticationRequest req = createRequest(userSuppliedIdentifier);
+ int reqIndex = 0;
+ foreach (var req in requests) {
+ req.AddCallbackArguments("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)["dotnetopenid.userSuppliedIdentifier"])) {
req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", userSuppliedIdentifier);
}
+ // 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.AddCallbackArguments("dotnetopenid.op_endpoint", req.Provider.Uri.AbsoluteUri);
+ req.AddCallbackArguments("dotnetopenid.claimed_id", req.ClaimedIdentifier);
req.AddCallbackArguments("dotnetopenid.phase", "2");
- if (query["dotnetopenid.immediate"] == "true") {
+ if (immediate) {
req.Mode = AuthenticationRequestMode.Immediate;
+ ((AuthenticationRequest)req).associationPreference = AssociationPreference.IfAlreadyEstablished;
}
- OnLoggingIn(req);
- req.RedirectToProvider();
- } catch (OpenIdException ex) {
- callbackUserAgentMethod("dnoi_internal.openidDiscoveryFailure('" + ex.Message.Replace("'", "\\'") + "')");
}
+
+ return requests;
}
- private void reportDiscoveryResult() {
+ private List<IAuthenticationRequest> RemoveDuplicateEndpoints(List<IAuthenticationRequest> requests) {
+ var filteredRequests = new List<IAuthenticationRequest>(requests.Count);
+ foreach (IAuthenticationRequest request in requests) {
+ // 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.
+ if (!Util.Contains(filteredRequests, req => string.Equals(req.Provider.Uri.Host, request.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase))) {
+ filteredRequests.Add(request);
+ }
+ }
+
+ return filteredRequests;
+ }
+
+ private void reportAuthenticationResult() {
Logger.InfoFormat("AJAX (iframe) callback from OP: {0}", Page.Request.Url);
List<string> assignments = new List<string>();
@@ -669,6 +728,7 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
var f = Util.NameValueCollectionToDictionary(HttpUtility.ParseQueryString(Page.Request.Url.Query));
var authResponse = RelyingParty.AuthenticationResponse.Parse(f, rp, Page.Request.Url, false);
if (authResponse.Status == AuthenticationStatus.Authenticated) {
+ OnUnconfirmedPositiveAssertion();
foreach (var pair in clientScriptExtensions) {
string js = authResponse.GetExtensionClientScript(pair.Key);
if (string.IsNullOrEmpty(js)) {
@@ -678,7 +738,7 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
}
}
- callbackUserAgentMethod("dnoi_internal.openidAuthResult(document.URL)", assignments.ToArray());
+ callbackUserAgentMethod("dnoi_internal.processAuthorizationResult(document.URL)", assignments.ToArray());
}
/// <summary>
@@ -706,7 +766,7 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
writer.WriteBeginTag("input");
writer.WriteAttribute("name", Name);
writer.WriteAttribute("id", ClientID);
- writer.WriteAttribute("value", Text);
+ writer.WriteAttribute("value", Text, true);
writer.WriteAttribute("size", Columns.ToString(CultureInfo.InvariantCulture));
if (TabIndex > 0) {
writer.WriteAttribute("tabindex", TabIndex.ToString(CultureInfo.InvariantCulture));
@@ -726,6 +786,17 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
writer.Write(" />");
writer.WriteEndTag("span");
+
+ // Emit a hidden field to let the javascript on the user agent know if an
+ // authentication has already successfully taken place.
+ string viewstateAuthData = ViewState[authDataViewStateKey] as string;
+ if (!string.IsNullOrEmpty(viewstateAuthData)) {
+ writer.WriteBeginTag("input");
+ writer.WriteAttribute("type", "hidden");
+ writer.WriteAttribute("name", openidAuthDataFormKey);
+ writer.WriteAttribute("value", viewstateAuthData, true);
+ writer.Write(" />");
+ }
}
/// <summary>
@@ -769,5 +840,76 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
</script></body></html>", methodCall));
Page.Response.End();
}
+
+ #region ICallbackEventHandler Members
+
+ private string discoveryResult;
+
+ /// <summary>
+ /// Returns the result of discovery on some Identifier passed to <see cref="ICallbackEventHandler.RaiseCallbackEvent"/>.
+ /// </summary>
+ /// <value>A whitespace delimited list of URLs that can be used to initiate authentication.</value>
+ string ICallbackEventHandler.GetCallbackResult() {
+ Page.Response.ContentType = "text/javascript";
+ return discoveryResult;
+ }
+
+ /// <summary>
+ /// Performs discovery on some OpenID Identifier. Called directly from the user agent via
+ /// AJAX callback mechanisms.
+ /// </summary>
+ /// <param name="eventArgument">The identifier to perform discovery on.</param>
+ void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) {
+ string userSuppliedIdentifier = eventArgument;
+
+ if (String.IsNullOrEmpty(userSuppliedIdentifier)) throw new ArgumentNullException("userSuppliedIdentifier");
+ Logger.InfoFormat("AJAX discovery on {0} requested.", userSuppliedIdentifier);
+
+ // 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 {
+ List<IAuthenticationRequest> requests = createRequests(userSuppliedIdentifier, true);
+ if (requests.Count > 0) {
+ discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", Util.GetSafeJavascriptValue(requests[0].ClaimedIdentifier));
+ discoveryResultBuilder.Append("requests: [");
+ foreach (IAuthenticationRequest request in requests) {
+ OnLoggingIn(request);
+ discoveryResultBuilder.Append("{");
+ discoveryResultBuilder.AppendFormat("endpoint: {0},", Util.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri));
+ request.Mode = AuthenticationRequestMode.Immediate;
+ Response response = (Response)request.RedirectingResponse;
+ discoveryResultBuilder.AppendFormat("immediate: {0},", Util.GetSafeJavascriptValue(response.IndirectMessageAsRequestUri.AbsoluteUri));
+ request.Mode = AuthenticationRequestMode.Setup;
+ response = (Response)request.RedirectingResponse;
+ discoveryResultBuilder.AppendFormat("setup: {0}", Util.GetSafeJavascriptValue(response.IndirectMessageAsRequestUri.AbsoluteUri));
+ discoveryResultBuilder.Append("},");
+ }
+ discoveryResultBuilder.Length -= 1; // trim off last comma
+ discoveryResultBuilder.Append("]");
+ } else {
+ discoveryResultBuilder.Append("requests: new Array(),");
+ discoveryResultBuilder.AppendFormat("error: {0}", Util.GetSafeJavascriptValue(Strings.OpenIdEndpointNotFound));
+ }
+ } catch (OpenIdException ex) {
+ discoveryResultBuilder.Append("requests: new Array(),");
+ discoveryResultBuilder.AppendFormat("error: {0}", Util.GetSafeJavascriptValue(ex.Message));
+ }
+ discoveryResultBuilder.Append("}");
+ discoveryResult = discoveryResultBuilder.ToString();
+ }
+
+ #endregion
}
}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js
index e2ef0a0..f4b49aa 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js
+++ b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js
@@ -1,18 +1,43 @@
// Options that can be set on the host page:
-// window.openid_visible_iframe = true; // causes the hidden iframe to show up
-// window.openid_trace = true; // causes lots of alert boxes
+//window.openid_visible_iframe = true; // causes the hidden iframe to show up
+//window.openid_trace = true; // causes lots of messages
function trace(msg) {
if (window.openid_trace) {
- alert(msg);
+ if (!window.tracediv) {
+ window.tracediv = document.createElement("ol");
+ document.body.appendChild(window.tracediv);
+ }
+ var el = document.createElement("li");
+ el.appendChild(document.createTextNode(msg));
+ window.tracediv.appendChild(el);
+ //alert(msg);
}
}
+/// <summary>Removes a given element from the array.</summary>
+/// <returns>True if the element was in the array, or false if it was not found.</returns>
+Array.prototype.remove = function(element) {
+ function elementToRemoveLast(a, b) {
+ if (a == element) { return 1; }
+ if (b == element) { return -1; }
+ return 0;
+ }
+ this.sort(elementToRemoveLast);
+ if (this[this.length - 1] == element) {
+ this.pop();
+ return true;
+ } else {
+ return false;
+ }
+};
+
function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url, success_icon_url, failure_icon_url,
- timeout, assertionReceivedCode,
+ throttle, timeout, assertionReceivedCode,
loginButtonText, loginButtonToolTip, retryButtonText, retryButtonToolTip, busyToolTip,
identifierRequiredMessage, loginInProgressMessage,
- authenticationSucceededToolTip, authenticationFailedToolTip) {
+ authenticatedByToolTip, authenticatedAsToolTip, authenticationFailedToolTip,
+ discoverCallback, discoveryFailedCallback) {
box.dnoi_internal = new Object();
if (assertionReceivedCode) {
box.dnoi_internal.onauthenticated = function(sender, e) { eval(assertionReceivedCode); }
@@ -20,12 +45,85 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
box.dnoi_internal.originalBackground = box.style.background;
box.timeout = timeout;
+ box.dnoi_internal.discoverIdentifier = discoverCallback;
+ box.dnoi_internal.authenticationRequests = new Array();
+
+ // The possible authentication results
+ var authSuccess = new Object();
+ var authRefused = new Object();
+ var timedOut = new Object();
+
+ function FrameManager(maxFrames) {
+ this.queuedWork = new Array();
+ this.frames = new Array();
+ this.maxFrames = maxFrames;
+
+ /// <summary>Called to queue up some work that will use an iframe as soon as it is available.</summary>
+ /// <param name="job">
+ /// A delegate that must return the url to point to iframe to.
+ /// Its first parameter is the iframe created to service the request.
+ /// It will only be called when the work actually begins.
+ /// </param>
+ this.enqueueWork = function(job) {
+ // Assign an iframe to this task immediately if there is one available.
+ if (this.frames.length < this.maxFrames) {
+ this.createIFrame(job);
+ } else {
+ this.queuedWork.unshift(job);
+ }
+ };
+
+ /// <summary>Clears the job queue and immediately closes all iframes.</summary>
+ this.cancelAllWork = function() {
+ trace('Canceling all open and pending iframes.');
+ while (this.queuedWork.pop());
+ this.closeFrames();
+ };
+
+ /// <summary>An event fired when a frame is closing.</summary>
+ this.onJobCompleted = function() {
+ // If there is a job in the queue, go ahead and start it up.
+ if (job = this.queuedWork.pop()) {
+ this.createIFrame(job);
+ }
+ }
+
+ this.createIFrame = function(job) {
+ var iframe = document.createElement("iframe");
+ if (!window.openid_visible_iframe) {
+ iframe.setAttribute("width", 0);
+ iframe.setAttribute("height", 0);
+ iframe.setAttribute("style", "display: none");
+ }
+ iframe.setAttribute("src", job(iframe));
+ iframe.openidBox = box;
+ box.parentNode.insertBefore(iframe, box);
+ this.frames.push(iframe);
+ return iframe;
+ };
+ this.closeFrames = function() {
+ if (this.frames.length == 0) { return false; }
+ for (var i = 0; i < this.frames.length; i++) {
+ if (this.frames[i].parentNode) { this.frames[i].parentNode.removeChild(this.frames[i]); }
+ }
+ while (this.frames.length > 0) { this.frames.pop(); }
+ return true;
+ };
+ this.closeFrame = function(frame) {
+ if (frame.parentNode) { frame.parentNode.removeChild(frame); }
+ var removed = this.frames.remove(frame);
+ this.onJobCompleted();
+ return removed;
+ };
+ }
+
+ box.dnoi_internal.authenticationIFrames = new FrameManager(throttle);
box.dnoi_internal.constructButton = function(text, tooltip, onclick) {
var button = document.createElement('button');
button.textContent = text; // Mozilla
button.value = text; // IE
- button.title = tooltip;
+ button.title = tooltip != null ? tooltip : '';
button.onclick = onclick;
button.style.visibility = 'hidden';
button.style.position = 'absolute';
@@ -42,6 +140,7 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
var icon = document.createElement('img');
icon.src = imageUrl;
icon.title = tooltip != null ? tooltip : '';
+ icon.originalTitle = icon.title;
if (!visible) {
icon.style.visibility = 'hidden';
}
@@ -68,51 +167,86 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
return img;
}
+ function findParentForm(element) {
+ if (element == null || element.nodeName == "FORM") {
+ return element;
+ }
+
+ return findParentForm(element.parentNode);
+ };
+
+ box.parentForm = findParentForm(box);
+
+ function findOrCreateHiddenField() {
+ var name = box.name + '_openidAuthData';
+ var existing = window.document.getElementsByName(name);
+ if (existing && existing.length > 0) {
+ return existing[0];
+ }
+
+ var hiddenField = document.createElement('input');
+ hiddenField.setAttribute("name", name);
+ hiddenField.setAttribute("type", "hidden");
+ box.parentForm.appendChild(hiddenField);
+ return hiddenField;
+ };
+
box.dnoi_internal.loginButton = box.dnoi_internal.constructButton(loginButtonText, loginButtonToolTip, function() {
- box.dnoi_internal.popup = window.open(box.dnoi_internal.getAuthenticationUrl(), 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,width=800,height=600');
- self.waiting_openidBox = box;
+ var discoveryInfo = box.dnoi_internal.authenticationRequests[box.lastDiscoveredIdentifier];
+ if (discoveryInfo == null) {
+ trace('Ooops! Somehow the login button click event was invoked, but no openid discovery information for ' + box.lastDiscoveredIdentifier + ' is available.');
+ return;
+ }
+ // The login button always sends a setup message to the first OP.
+ var selectedProvider = discoveryInfo[0];
+ selectedProvider.trySetup();
return false;
});
box.dnoi_internal.retryButton = box.dnoi_internal.constructButton(retryButtonText, retryButtonToolTip, function() {
box.timeout += 5000; // give the retry attempt 5s longer than the last attempt
- box.dnoi_internal.performDiscovery();
+ box.dnoi_internal.performDiscovery(box.value);
return false;
});
box.dnoi_internal.openid_logo = box.dnoi_internal.constructIcon(openid_logo_url, null, false, true);
- box.dnoi_internal.op_logo = box.dnoi_internal.constructIcon('', null, false, false, "16px");
+ box.dnoi_internal.op_logo = box.dnoi_internal.constructIcon('', authenticatedByToolTip, false, false, "16px");
box.dnoi_internal.spinner = box.dnoi_internal.constructIcon(spinner_url, busyToolTip, true);
- box.dnoi_internal.success_icon = box.dnoi_internal.constructIcon(success_icon_url, authenticationSucceededToolTip, true);
+ box.dnoi_internal.success_icon = box.dnoi_internal.constructIcon(success_icon_url, authenticatedAsToolTip, true);
//box.dnoi_internal.failure_icon = box.dnoi_internal.constructIcon(failure_icon_url, authenticationFailedToolTip, true);
// Disable the display of the DotNetOpenId logo
//box.dnoi_internal.dnoi_logo = box.dnoi_internal.constructIcon(dotnetopenid_logo_url);
box.dnoi_internal.dnoi_logo = box.dnoi_internal.openid_logo;
- box.dnoi_internal.setVisualCue = function(state) {
+ box.dnoi_internal.setVisualCue = function(state, authenticatedBy, authenticatedAs) {
box.dnoi_internal.openid_logo.style.visibility = 'hidden';
box.dnoi_internal.dnoi_logo.style.visibility = 'hidden';
box.dnoi_internal.op_logo.style.visibility = 'hidden';
+ box.dnoi_internal.openid_logo.title = box.dnoi_internal.openid_logo.originalTitle;
box.dnoi_internal.spinner.style.visibility = 'hidden';
box.dnoi_internal.success_icon.style.visibility = 'hidden';
-// box.dnoi_internal.failure_icon.style.visibility = 'hidden';
+ // box.dnoi_internal.failure_icon.style.visibility = 'hidden';
box.dnoi_internal.loginButton.style.visibility = 'hidden';
box.dnoi_internal.retryButton.style.visibility = 'hidden';
- box.title = null;
+ box.title = '';
+ box.dnoi_internal.state = state;
if (state == "discovering") {
box.dnoi_internal.dnoi_logo.style.visibility = 'visible';
box.dnoi_internal.spinner.style.visibility = 'visible';
box.dnoi_internal.claimedIdentifier = null;
- box.title = null;
+ box.title = '';
window.status = "Discovering OpenID Identifier '" + box.value + "'...";
} else if (state == "authenticated") {
var opLogo = box.dnoi_internal.deriveOPFavIcon();
if (opLogo) {
box.dnoi_internal.op_logo.src = opLogo;
box.dnoi_internal.op_logo.style.visibility = 'visible';
+ box.dnoi_internal.op_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost());
} else {
box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.dnoi_internal.openid_logo.title = box.dnoi_internal.op_logo.originalTitle.replace('{0}', authenticatedBy.getHost());
}
box.dnoi_internal.success_icon.style.visibility = 'visible';
+ box.dnoi_internal.success_icon.title = box.dnoi_internal.success_icon.originalTitle.replace('{0}', authenticatedAs);
box.title = box.dnoi_internal.claimedIdentifier;
window.status = "Authenticated as " + box.value;
} else if (state == "setup") {
@@ -135,7 +269,7 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
box.title = authenticationFailedToolTip;
} else if (state = '' || state == null) {
box.dnoi_internal.openid_logo.style.visibility = 'visible';
- box.title = null;
+ box.title = '';
box.dnoi_internal.claimedIdentifier = null;
window.status = null;
} else {
@@ -145,18 +279,40 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
}
box.dnoi_internal.isBusy = function() {
- return box.discoveryIFrame != null;
+ return box.dnoi_internal.state == 'discovering' ||
+ box.dnoi_internal.authenticationRequests[box.lastDiscoveredIdentifier].busy();
};
+ box.dnoi_internal.canAttemptLogin = function() {
+ if (box.value.length == 0) return false;
+ if (box.dnoi_internal.authenticationRequests[box.value] == null) return false;
+ if (box.dnoi_internal.state == 'failed') return false;
+ return true;
+ };
+
+ box.dnoi_internal.getUserSuppliedIdentifierResults = function() {
+ return box.dnoi_internal.authenticationRequests[box.value];
+ }
+
+ box.dnoi_internal.isAuthenticated = function() {
+ var results = box.dnoi_internal.getUserSuppliedIdentifierResults();
+ return results != null && results.findSuccessfulRequest() != null;
+ }
+
box.dnoi_internal.onSubmit = function() {
- if (box.lastAuthenticationResult != 'authenticated') {
+ var hiddenField = findOrCreateHiddenField();
+ if (box.dnoi_internal.isAuthenticated()) {
+ // stick the result in a hidden field so the RP can verify it
+ hiddenField.setAttribute("value", box.dnoi_internal.authenticationRequests[box.value].successAuthData);
+ } else {
+ hiddenField.setAttribute("value", '');
if (box.dnoi_internal.isBusy()) {
alert(loginInProgressMessage);
} else {
if (box.value.length > 0) {
// submitPending will be true if we've already tried deferring submit for a login,
// in which case we just want to display a box to the user.
- if (box.dnoi_internal.submitPending) {
+ if (box.dnoi_internal.submitPending || !box.dnoi_internal.canAttemptLogin()) {
alert(identifierRequiredMessage);
} else {
// The user hasn't clicked "Login" yet. We'll click login for him,
@@ -178,6 +334,11 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
return true;
};
+ /// <summary>
+ /// Records which submit button caused this openid box to question whether it
+ /// was ready to submit the user's identifier so that that button can be re-invoked
+ /// automatically after authentication completes.
+ /// </summary>
box.dnoi_internal.setLastSubmitButtonClicked = function(evt) {
var button;
if (evt.target) {
@@ -189,70 +350,32 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
box.dnoi_internal.submitButtonJustClicked = button;
};
- // box.hookAllSubmitElements = function(searchNode) {
- var inputs = document.getElementsByTagName('input');
- for (var i = 0; i < inputs.length; i++) {
- var el = inputs[i];
- if (el.type == 'submit') {
- if (el.attachEvent) {
- el.attachEvent("onclick", box.dnoi_internal.setLastSubmitButtonClicked);
- } else {
- el.addEventListener("click", box.dnoi_internal.setLastSubmitButtonClicked, true);
- }
+ // Find all submit buttons and hook their click events so that we can validate
+ // whether we are ready for the user to postback.
+ var inputs = document.getElementsByTagName('input');
+ for (var i = 0; i < inputs.length; i++) {
+ var el = inputs[i];
+ if (el.type == 'submit') {
+ if (el.attachEvent) {
+ el.attachEvent("onclick", box.dnoi_internal.setLastSubmitButtonClicked);
+ } else {
+ el.addEventListener("click", box.dnoi_internal.setLastSubmitButtonClicked, true);
}
}
- //};
-
- box.dnoi_internal.getAuthenticationUrl = function(immediateMode) {
- var frameLocation = new Uri(document.location.href);
- var discoveryUri = frameLocation.trimFragment();
- discoveryUri.appendQueryVariable('dotnetopenid.userSuppliedIdentifier', box.value);
- if (immediateMode) {
- discoveryUri.appendQueryVariable('dotnetopenid.immediate', 'true');
- }
- return discoveryUri;
- };
-
- box.dnoi_internal.performDiscovery = function() {
- box.dnoi_internal.closeDiscoveryIFrame();
- box.dnoi_internal.setVisualCue('discovering');
- box.lastDiscoveredIdentifier = box.value;
- box.lastAuthenticationResult = null;
- var discoveryUri = box.dnoi_internal.getAuthenticationUrl(true);
- if (box.discoveryIFrame) {
- box.discoveryIFrame.parentNode.removeChild(box.discoveryIFrame);
- box.discoveryIFrame = null;
- }
- trace('Performing discovery using url: ' + discoveryUri);
- box.discoveryIFrame = createHiddenFrame(discoveryUri);
- };
-
- function findParentForm(element) {
- if (element == null || element.nodeName == "FORM") {
- return element;
- }
-
- return findParentForm(element.parentNode);
- };
-
- function findOrCreateHiddenField(form, name) {
- if (box.hiddenField) {
- return box.hiddenField;
- }
-
- box.hiddenField = document.createElement('input');
- box.hiddenField.setAttribute("name", name);
- box.hiddenField.setAttribute("type", "hidden");
- form.appendChild(box.hiddenField);
- return box.hiddenField;
- };
+ }
+ /// <summary>
+ /// Returns the URL of the authenticating OP's logo so it can be displayed to the user.
+ /// </summary>
box.dnoi_internal.deriveOPFavIcon = function() {
- if (!box.hiddenField) return;
- var authResult = new Uri(box.hiddenField.value);
+ var response = box.dnoi_internal.getUserSuppliedIdentifierResults().successAuthData;
+ if (!response || response.length == 0) return;
+ var authResult = new Uri(response);
var opUri;
if (authResult.getQueryArgValue("openid.op_endpoint")) {
opUri = new Uri(authResult.getQueryArgValue("openid.op_endpoint"));
+ } if (authResult.getQueryArgValue("dotnetopenid.op_endpoint")) {
+ opUri = new Uri(authResult.getQueryArgValue("dotnetopenid.op_endpoint"));
} else if (authResult.getQueryArgValue("openid.user_setup_url")) {
opUri = new Uri(authResult.getQueryArgValue("openid.user_setup_url"));
} else return null;
@@ -260,66 +383,207 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
return favicon;
};
- function createHiddenFrame(url) {
- var iframe = document.createElement("iframe");
- if (!window.openid_visible_iframe) {
- iframe.setAttribute("width", 0);
- iframe.setAttribute("height", 0);
- iframe.setAttribute("style", "display: none");
+ box.dnoi_internal.createDiscoveryInfo = function(discoveryInfo, identifier) {
+ this.identifier = identifier;
+ // The claimed identifier may be null if the user provided an OP Identifier.
+ this.claimedIdentifier = discoveryInfo.claimedIdentifier;
+ trace('Discovered claimed identifier: ' + this.claimedIdentifier);
+
+ // Add extra tracking bits and behaviors.
+ this.findByEndpoint = function(opEndpoint) {
+ for (var i = 0; i < this.length; i++) {
+ if (this[i].endpoint == opEndpoint) {
+ return this[i];
+ }
+ }
+ };
+ this.findSuccessfulRequest = function() {
+ for (var i = 0; i < this.length; i++) {
+ if (this[i].result == authSuccess) {
+ return this[i];
+ }
+ }
+ };
+ this.busy = function() {
+ for (var i = 0; i < this.length; i++) {
+ if (this[i].busy()) {
+ return true;
+ }
+ }
+ };
+ this.abortAll = function() {
+ // Abort all other asynchronous authentication attempts that may be in progress.
+ box.dnoi_internal.authenticationIFrames.cancelAllWork();
+ for (var i = 0; i < this.length; i++) {
+ this[i].abort();
+ }
+ };
+ this.tryImmediate = function() {
+ if (this.length > 0) {
+ for (var i = 0; i < this.length; i++) {
+ box.dnoi_internal.authenticationIFrames.enqueueWork(this[i].tryImmediate);
+ }
+ } else {
+ box.dnoi_internal.discoveryFailed(null, this.identifier);
+ }
+ };
+
+ this.length = discoveryInfo.requests.length;
+ for (var i = 0; i < discoveryInfo.requests.length; i++) {
+ this[i] = new box.dnoi_internal.createTrackingRequest(discoveryInfo.requests[i], identifier);
}
- iframe.setAttribute("src", url);
- iframe.openidBox = box;
- box.parentNode.insertBefore(iframe, box);
- box.discoveryTimeout = setTimeout(function() { trace("timeout"); box.dnoi_internal.openidDiscoveryFailure("Timed out"); }, box.timeout);
- return iframe;
};
- box.parentForm = findParentForm(box);
+ box.dnoi_internal.createTrackingRequest = function(requestInfo, identifier) {
+ // It's possible during a postback that discovered request URLs are not available.
+ this.immediate = requestInfo.immediate ? new Uri(requestInfo.immediate) : null;
+ this.setup = requestInfo.setup ? new Uri(requestInfo.setup) : null;
+ this.endpoint = new Uri(requestInfo.endpoint);
+ this.identifier = identifier;
+ var self = this; // closure so that delegates have the right instance
- box.dnoi_internal.openidDiscoveryFailure = function(msg) {
- box.dnoi_internal.closeDiscoveryIFrame();
- trace('Discovery failure: ' + msg);
- box.lastAuthenticationResult = 'failed';
- box.dnoi_internal.setVisualCue('failed');
- box.title = msg;
- };
+ this.host = self.endpoint.getHost();
- box.dnoi_internal.closeDiscoveryIFrame = function() {
- if (box.discoveryTimeout) {
- clearTimeout(box.discoveryTimeout);
- }
- if (box.discoveryIFrame) {
- box.discoveryIFrame.parentNode.removeChild(box.discoveryIFrame);
- box.discoveryIFrame = null;
+ this.getDiscoveryInfo = function() {
+ return box.dnoi_internal.authenticationRequests[self.identifier];
}
+
+ this.busy = function() {
+ return self.iframe != null || self.popup != null;
+ };
+
+ this.completeAttempt = function() {
+ if (!self.busy()) return false;
+ if (self.iframe) {
+ trace('iframe hosting ' + self.endpoint + ' now CLOSING.');
+ box.dnoi_internal.authenticationIFrames.closeFrame(self.iframe);
+ self.iframe = null;
+ }
+ if (self.popup) {
+ self.popup.close();
+ self.popup = null;
+ }
+ if (self.timeout) {
+ window.clearTimeout(self.timeout);
+ self.timeout = null;
+ }
+
+ if (!self.getDiscoveryInfo().busy() && self.getDiscoveryInfo().findSuccessfulRequest() == null) {
+ trace('No asynchronous authentication attempt is in progress. Display setup view.');
+ // visual cue that auth failed
+ box.dnoi_internal.setVisualCue('setup');
+ }
+
+ return true;
+ };
+
+ this.authenticationTimedOut = function() {
+ if (self.completeAttempt()) {
+ trace(self.host + " timed out");
+ self.result = timedOut;
+ }
+ };
+ this.authSuccess = function(authUri) {
+ if (self.completeAttempt()) {
+ trace(self.host + " authenticated!");
+ self.result = authSuccess;
+ self.response = authUri;
+ box.dnoi_internal.authenticationRequests[self.identifier].abortAll();
+ }
+ };
+ this.authFailed = function() {
+ if (self.completeAttempt()) {
+ //trace(self.host + " failed authentication");
+ self.result = authRefused;
+ }
+ };
+ this.abort = function() {
+ if (self.completeAttempt()) {
+ trace(self.host + " aborted");
+ // leave the result as whatever it was before.
+ }
+ };
+
+ this.tryImmediate = function(iframe) {
+ self.abort(); // ensure no concurrent attempts
+ self.timeout = setTimeout(function() { self.authenticationTimedOut(); }, box.timeout);
+ trace('iframe hosting ' + self.endpoint + ' now OPENING.');
+ self.iframe = iframe;
+ //trace('initiating auth attempt with: ' + self.immediate);
+ return self.immediate;
+ };
+ this.trySetup = function() {
+ self.abort(); // ensure no concurrent attempts
+ window.waiting_openidBox = box;
+ self.popup = window.open(self.setup, 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,width=800,height=600');
+ };
};
- box.dnoi_internal.openidAuthResult = function(resultUrl) {
- self.waiting_openidBox = null;
- trace('openidAuthResult ' + resultUrl);
- if (box.discoveryIFrame) {
- box.dnoi_internal.closeDiscoveryIFrame();
- } else if (box.dnoi_internal.popup) {
- box.dnoi_internal.popup.close();
- box.dnoi_internal.popup = null;
+ /*****************************************
+ * Flow
+ *****************************************/
+
+ /// <summary>Called to initiate discovery on some identifier.</summary>
+ box.dnoi_internal.performDiscovery = function(identifier) {
+ box.dnoi_internal.authenticationIFrames.closeFrames();
+ box.dnoi_internal.setVisualCue('discovering');
+ box.lastDiscoveredIdentifier = identifier;
+ box.dnoi_internal.discoverIdentifier(identifier, box.dnoi_internal.discoveryResult, box.dnoi_internal.discoveryFailed);
+ };
+
+ /// <summary>Callback that is invoked when discovery fails.</summary>
+ box.dnoi_internal.discoveryFailed = function(message, identifier) {
+ box.dnoi_internal.setVisualCue('failed');
+ if (message) { box.title = message; }
+ }
+
+ /// <summary>Callback that is invoked when discovery results are available.</summary>
+ /// <param name="discoveryResult">The JSON object containing the OpenID auth requests.</param>
+ /// <param name="identifier">The identifier that discovery was performed on.</param>
+ box.dnoi_internal.discoveryResult = function(discoveryResult, identifier) {
+ // Deserialize the JSON object and store the result if it was a successful discovery.
+ discoveryResult = eval('(' + discoveryResult + ')');
+ // Store the discovery results and added behavior for later use.
+ box.dnoi_internal.authenticationRequests[identifier] = discoveryBehavior = new box.dnoi_internal.createDiscoveryInfo(discoveryResult, identifier);
+
+ // Only act on the discovery event if we're still interested in the result.
+ // If the user already changed the identifier since discovery was initiated,
+ // we aren't interested in it any more.
+ if (identifier == box.lastDiscoveredIdentifier) {
+ discoveryBehavior.tryImmediate();
}
+ }
+
+ /// <summary>Invoked by RP web server when an authentication has completed.</summary>
+ /// <remarks>The duty of this method is to distribute the notification to the appropriate tracking object.</remarks>
+ box.dnoi_internal.processAuthorizationResult = function(resultUrl) {
+ self.waiting_openidBox = null;
+ //trace('processAuthorizationResult ' + resultUrl);
var resultUri = new Uri(resultUrl);
- // stick the result in a hidden field so the RP can verify it (positive or negative)
- var form = findParentForm(box);
- var hiddenField = findOrCreateHiddenField(form, "openidAuthData");
- hiddenField.setAttribute("value", resultUri.toString());
- trace("set openidAuthData = " + resultUri.queryString);
- if (hiddenField.parentNode == null) {
- form.appendChild(hiddenField);
+ // Find the tracking object responsible for this request.
+ var discoveryInfo = box.dnoi_internal.authenticationRequests[resultUri.getQueryArgValue('dotnetopenid.userSuppliedIdentifier')];
+ if (discoveryInfo == null) {
+ trace('processAuthorizationResult called but no userSuppliedIdentifier parameter was found. Exiting function.');
+ return;
}
- trace("review: " + box.hiddenField.value);
+ var opEndpoint = resultUri.getQueryArgValue("openid.op_endpoint") ? resultUri.getQueryArgValue("openid.op_endpoint") : resultUri.getQueryArgValue("dotnetopenid.op_endpoint");
+ var tracker = discoveryInfo.findByEndpoint(opEndpoint);
+ //trace('Auth result for ' + tracker.host + ' received:\n' + resultUrl);
if (isAuthSuccessful(resultUri)) {
+ tracker.authSuccess(resultUri);
+
+ discoveryInfo.successAuthData = resultUrl;
+ var claimed_id = resultUri.getQueryArgValue("openid.claimed_id");
+ if (claimed_id && claimed_id != discoveryInfo.claimedIdentifier) {
+ discoveryInfo.claimedIdentifier = resultUri.getQueryArgValue("openid.claimed_id");
+ trace('Authenticated as ' + claimed_id);
+ }
+
// visual cue that auth was successful
- box.dnoi_internal.claimedIdentifier = isOpenID2Response(resultUri) ? resultUri.getQueryArgValue("openid.claimed_id") : resultUri.getQueryArgValue("openid.identity");
- box.dnoi_internal.setVisualCue('authenticated');
- box.lastAuthenticationResult = 'authenticated';
+ box.dnoi_internal.claimedIdentifier = discoveryInfo.claimedIdentifier;
+ box.dnoi_internal.setVisualCue('authenticated', tracker.endpoint, discoveryInfo.claimedIdentifier);
if (box.dnoi_internal.onauthenticated) {
box.dnoi_internal.onauthenticated(box);
}
@@ -333,9 +597,7 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
}
}
} else {
- // visual cue that auth failed
- box.dnoi_internal.setVisualCue('setup');
- box.lastAuthenticationResult = 'setup';
+ tracker.authFailed();
}
box.dnoi_internal.submitPending = null;
@@ -354,25 +616,46 @@ function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url
};
box.onblur = function(event) {
- if (box.lastDiscoveredIdentifier != box.value) {
+ var discoveryInfo = box.dnoi_internal.authenticationRequests[box.value];
+ if (discoveryInfo == null) {
if (box.value.length > 0) {
- box.dnoi_internal.performDiscovery();
+ box.dnoi_internal.performDiscovery(box.value);
} else {
box.dnoi_internal.setVisualCue();
}
- box.oldvalue = box.value;
+ } else {
+ if ((priorSuccess = discoveryInfo.findSuccessfulRequest())) {
+ box.dnoi_internal.setVisualCue('authenticated', priorSuccess.endpoint, discoveryInfo.claimedIdentifier);
+ } else {
+ discoveryInfo.tryImmediate();
+ }
}
return true;
};
box.onkeyup = function(event) {
- if (box.lastDiscoveredIdentifier != box.value) {
- box.dnoi_internal.setVisualCue();
- } else {
- box.dnoi_internal.setVisualCue(box.lastAuthenticationResult);
- }
+ box.dnoi_internal.setVisualCue();
return true;
};
+
box.getClaimedIdentifier = function() { return box.dnoi_internal.claimedIdentifier; };
+
+ // Restore a previously achieved state (from pre-postback) if it is given.
+ var oldAuth = findOrCreateHiddenField().value;
+ if (oldAuth.length > 0) {
+ var oldAuthResult = new Uri(oldAuth);
+ // The control ensures that we ALWAYS have an OpenID 2.0-style claimed_id attribute, even against
+ // 1.0 Providers via the return_to URL mechanism.
+ var claimedId = oldAuthResult.getQueryArgValue("dotnetopenid.claimed_id");
+ var endpoint = oldAuthResult.getQueryArgValue("dotnetopenid.op_endpoint");
+ // We weren't given a full discovery history, but we can spoof this much from the
+ // authentication assertion.
+ box.dnoi_internal.authenticationRequests[box.value] = new box.dnoi_internal.createDiscoveryInfo({
+ claimedIdentifier: claimedId,
+ requests: [{ endpoint: endpoint }]
+ }, box.value);
+
+ box.dnoi_internal.processAuthorizationResult(oldAuthResult.toString());
+ }
}
function Uri(url) {
@@ -422,7 +705,7 @@ function Uri(url) {
this.value = value;
};
- this.Pairs = Array();
+ this.Pairs = new Array();
var queryBeginsAt = this.originalUri.indexOf('?');
if (queryBeginsAt >= 0) {
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs
index 564f0e7..6328beb 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs
@@ -34,6 +34,7 @@ namespace DotNetOpenId.RelyingParty
HyperLink registerLink;
CheckBox rememberMeCheckBox;
Literal idselectorJavascript;
+ Label errorLabel;
const short textBoxTabIndexOffset = 0;
const short loginButtonTabIndexOffset = 1;
@@ -121,6 +122,12 @@ namespace DotNetOpenId.RelyingParty
identifierFormatValidator.ControlToValidate = WrappedTextBox.ID;
identifierFormatValidator.ValidationGroup = validationGroupDefault;
cell.Controls.Add(identifierFormatValidator);
+ errorLabel = new Label();
+ errorLabel.EnableViewState = false;
+ errorLabel.ForeColor = System.Drawing.Color.Red;
+ errorLabel.Style[HtmlTextWriterStyle.Display] = "block"; // puts it on its own line
+ errorLabel.Visible = false;
+ cell.Controls.Add(errorLabel);
examplePrefixLabel = new Label();
examplePrefixLabel.Text = examplePrefixDefault;
cell.Controls.Add(examplePrefixLabel);
@@ -210,6 +217,30 @@ idselector_input_id = '" + WrappedTextBox.ClientID + @"';
base.RenderChildren(writer);
}
+ /// <summary>
+ /// Adds failure handling to display an error message to the user.
+ /// </summary>
+ protected override void OnFailed(IAuthenticationResponse response) {
+ base.OnFailed(response);
+
+ if (!string.IsNullOrEmpty(FailedMessageText)) {
+ errorLabel.Text = string.Format(FailedMessageText, response.Exception.Message);
+ errorLabel.Visible = true;
+ }
+ }
+
+ /// <summary>
+ /// Adds authentication cancellation behavior to display a message to the user.
+ /// </summary>
+ protected override void OnCanceled(IAuthenticationResponse response) {
+ base.OnCanceled(response);
+
+ if (!string.IsNullOrEmpty(CanceledText)) {
+ errorLabel.Text = CanceledText;
+ errorLabel.Visible = true;
+ }
+ }
+
#region Properties
const string labelTextDefault = "OpenID Login:";
/// <summary>
@@ -391,6 +422,36 @@ idselector_input_id = '" + WrappedTextBox.ClientID + @"';
set { rememberMeCheckBox.Text = value; }
}
+ const string failedMessageTextViewStateKey = "FailedMessageText";
+ const string failedMessageTextDefault = "Login failed: {0}";
+ /// <summary>
+ /// Gets or sets the message display in the event of a failed authentication. {0} may be used to insert the actual error.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(failedMessageTextDefault)]
+ [Localizable(true)]
+ [Description("The message display in the event of a failed authentication. {0} may be used to insert the actual error.")]
+ public string FailedMessageText {
+ get { return (string)ViewState[failedMessageTextViewStateKey] ?? failedMessageTextDefault; }
+ set { ViewState[failedMessageTextViewStateKey] = value; }
+ }
+
+ const string canceledTextViewStateKey = "CanceledText";
+ const string canceledTextDefault = "Login canceled.";
+ /// <summary>
+ /// Gets or sets the text to display in the event of an authentication canceled at the Provider.
+ /// </summary>
+ [Bindable(true)]
+ [Category("Appearance")]
+ [DefaultValue(canceledTextDefault)]
+ [Localizable(true)]
+ [Description("The text to display in the event of an authentication canceled at the Provider.")]
+ public string CanceledText {
+ get { return (string)ViewState[canceledTextViewStateKey] ?? canceledTextDefault; }
+ set { ViewState[canceledTextViewStateKey] = value; }
+ }
+
const bool rememberMeVisibleDefault = false;
/// <summary>
/// Whether the "Remember Me" checkbox should be displayed.
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
index 0cbb4c3..fc763eb 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -172,8 +172,97 @@ namespace DotNetOpenId.RelyingParty {
/// An authentication request object that describes the HTTP response to
/// send to the user agent to initiate the authentication.
/// </returns>
+ /// <exception cref="OpenIdException">Thrown if no OpenID endpoint could be found.</exception>
public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
- if (userSuppliedIdentifier == null) throw new ArgumentNullException("userSuppliedIdentifier");
+ var requests = CreateRequests(userSuppliedIdentifier, realm, returnToUrl).GetEnumerator();
+ if (requests.MoveNext()) {
+ return requests.Current;
+ } else {
+ throw new OpenIdException(Strings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given 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>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// This method requires an ASP.NET HttpContext.
+ /// </remarks>
+ /// <exception cref="OpenIdException">Thrown if no OpenID endpoint could be found.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
+ var requests = CreateRequests(userSuppliedIdentifier, realm).GetEnumerator();
+ if (requests.MoveNext()) {
+ return requests.Current;
+ } else {
+ throw new OpenIdException(Strings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// This method requires an ASP.NET HttpContext.
+ /// </remarks>
+ /// <exception cref="OpenIdException">Thrown if no OpenID endpoint could be found.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
+ var requests = CreateRequests(userSuppliedIdentifier).GetEnumerator();
+ if (requests.MoveNext()) {
+ return requests.Current;
+ } else {
+ throw new OpenIdException(Strings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Generates the 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>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </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>
+ internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
if (realm == null) throw new ArgumentNullException("realm");
if (returnToUrl == null) throw new ArgumentNullException("returnToUrl");
@@ -186,12 +275,11 @@ namespace DotNetOpenId.RelyingParty {
returnTo.Path = realm.AbsolutePath + returnTo.Path.Substring(realm.AbsolutePath.Length);
}
- return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnTo.Uri);
+ return Util.Cast<IAuthenticationRequest>(AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnTo.Uri, true));
}
/// <summary>
- /// Creates an authentication request to verify that a user controls
- /// some given Identifier.
+ /// Generates the 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.
@@ -206,9 +294,14 @@ namespace DotNetOpenId.RelyingParty {
/// send to the user agent to initiate the authentication.
/// </returns>
/// <remarks>
- /// This method requires an ASP.NET HttpContext.
+ /// <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 IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
+ internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
// Build the return_to URL
@@ -225,18 +318,11 @@ namespace DotNetOpenId.RelyingParty {
}
UriUtil.AppendQueryArgs(returnTo, returnToParams);
- return CreateRequest(userSuppliedIdentifier, realm, returnTo.Uri);
- }
-
- internal static bool ShouldParameterBeStrippedFromReturnToUrl(string parameterName) {
- Protocol protocol = Protocol.Default;
- return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
- || parameterName == Token.TokenKey;
+ return CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
}
/// <summary>
- /// Creates an authentication request to verify that a user controls
- /// some given Identifier.
+ /// Generates the 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.
@@ -246,9 +332,14 @@ namespace DotNetOpenId.RelyingParty {
/// send to the user agent to initiate the authentication.
/// </returns>
/// <remarks>
- /// This method requires an ASP.NET HttpContext.
+ /// <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 IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
+ internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
// Build the realm URL
@@ -264,7 +355,13 @@ namespace DotNetOpenId.RelyingParty {
if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal))
realmUrl.Path += "/";
- return CreateRequest(userSuppliedIdentifier, new Realm(realmUrl.Uri));
+ return CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri));
+ }
+
+ internal static bool ShouldParameterBeStrippedFromReturnToUrl(string parameterName) {
+ Protocol protocol = Protocol.Default;
+ return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
+ || parameterName == Token.TokenKey;
}
/// <summary>
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs
index 55861d4..de45a30 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs
@@ -251,6 +251,20 @@ namespace DotNetOpenId.RelyingParty
set { ViewState[showLogoViewStateKey] = value; }
}
+ const string presetBorderViewStateKey = "PresetBorder";
+ const bool presetBorderDefault = true;
+ /// <summary>
+ /// Gets/sets whether to use inline styling to force a solid gray border.
+ /// </summary>
+ [Bindable(true)]
+ [Category(appearanceCategory)]
+ [DefaultValue(presetBorderDefault)]
+ [Description("Whether to use inline styling to force a solid gray border.")]
+ public bool PresetBorder {
+ get { return (bool)(ViewState[presetBorderViewStateKey] ?? presetBorderDefault); }
+ set { ViewState[presetBorderViewStateKey] = value; }
+ }
+
const string usePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie";
const string usePersistentCookieViewStateKey = "UsePersistentCookie";
/// <summary>
@@ -685,14 +699,17 @@ namespace DotNetOpenId.RelyingParty
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
- if (ShowLogo)
- {
+ if (ShowLogo) {
string logoUrl = Page.ClientScript.GetWebResourceUrl(
typeof(OpenIdTextBox), EmbeddedLogoResourceName);
- WrappedTextBox.Style["background"] = string.Format(CultureInfo.InvariantCulture,
- "#fff url({0}) no-repeat", HttpUtility.HtmlEncode(logoUrl));
+ WrappedTextBox.Style[HtmlTextWriterStyle.BackgroundImage] = string.Format(
+ CultureInfo.InvariantCulture, "url({0})", HttpUtility.HtmlEncode(logoUrl));
+ WrappedTextBox.Style["background-repeat"] = "no-repeat";
WrappedTextBox.Style["background-position"] = "0 50%";
WrappedTextBox.Style[HtmlTextWriterStyle.PaddingLeft] = "18px";
+ }
+
+ if (PresetBorder) {
WrappedTextBox.Style[HtmlTextWriterStyle.BorderStyle] = "solid";
WrappedTextBox.Style[HtmlTextWriterStyle.BorderWidth] = "1px";
WrappedTextBox.Style[HtmlTextWriterStyle.BorderColor] = "lightgray";
diff --git a/src/DotNetOpenId/Response.cs b/src/DotNetOpenId/Response.cs
index 112aa4b..7616167 100644
--- a/src/DotNetOpenId/Response.cs
+++ b/src/DotNetOpenId/Response.cs
@@ -50,5 +50,20 @@ namespace DotNetOpenId {
HttpContext.Current.Response.OutputStream.Close();
HttpContext.Current.Response.End();
}
+
+ /// <summary>
+ /// Gets the indirect message as it would appear as a single URI request.
+ /// </summary>
+ internal Uri IndirectMessageAsRequestUri {
+ get {
+ if (EncodableMessage != null && EncodableMessage.RedirectUrl != null && EncodableMessage.EncodingType == EncodingType.IndirectMessage) {
+ UriBuilder builder = new UriBuilder(EncodableMessage.RedirectUrl);
+ UriUtil.AppendQueryArgs(builder, EncodableMessage.EncodedFields);
+ return builder.Uri;
+ } else {
+ throw new InvalidOperationException();
+ }
+ }
+ }
}
}
diff --git a/src/DotNetOpenId/Strings.Designer.cs b/src/DotNetOpenId/Strings.Designer.cs
index 0af791f..33586a8 100644
--- a/src/DotNetOpenId/Strings.Designer.cs
+++ b/src/DotNetOpenId/Strings.Designer.cs
@@ -614,6 +614,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to Invalid birthdate value. Must be in the form yyyy-MM-dd..
+ /// </summary>
+ internal static string SregInvalidBirthdate {
+ get {
+ return ResourceManager.GetString("SregInvalidBirthdate", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The &apos;{0}&apos; parameter was expected to have the value &apos;{1}&apos; but had &apos;{2}&apos; instead..
/// </summary>
internal static string TamperingDetected {
diff --git a/src/DotNetOpenId/Strings.resx b/src/DotNetOpenId/Strings.resx
index 6bebc76..37e8d52 100644
--- a/src/DotNetOpenId/Strings.resx
+++ b/src/DotNetOpenId/Strings.resx
@@ -322,10 +322,13 @@ Discovered endpoint info:
<data name="XriResolutionStatusMissing" xml:space="preserve">
<value>Could not find XRI resolution Status tag or code attribute was invalid.</value>
</data>
+ <data name="SregInvalidBirthdate" xml:space="preserve">
+ <value>Invalid birthdate value. Must be in the form yyyy-MM-dd.</value>
+ </data>
<data name="ClaimedIdentifierCannotBeSetOnDelegatedAuthentication" xml:space="preserve">
<value>The ClaimedIdentifier property cannot be set when IsDelegatedIdentifier is true to avoid breaking OpenID URL delegation.</value>
</data>
<data name="OpenIdDiscoveredAndActualVersionMismatch" xml:space="preserve">
<value>Positive assertion sent with OpenID version {0} but Identifier discovery suggested it would be {1}.</value>
</data>
-</root> \ No newline at end of file
+</root>
diff --git a/src/DotNetOpenId/UntrustedWebRequest.cs b/src/DotNetOpenId/UntrustedWebRequest.cs
index a621a65..4ba07af 100644
--- a/src/DotNetOpenId/UntrustedWebRequest.cs
+++ b/src/DotNetOpenId/UntrustedWebRequest.cs
@@ -8,6 +8,7 @@ namespace DotNetOpenId {
using System.Globalization;
using System.IO;
using System.Net;
+ using System.Net.Cache;
using System.Text.RegularExpressions;
using System.Configuration;
using DotNetOpenId.Configuration;
@@ -70,6 +71,16 @@ namespace DotNetOpenId {
/// </summary>
public static TimeSpan Timeout { get; set; }
+ /// <summary>
+ /// Gets or sets the default cache policy to use for HTTP requests.
+ /// </summary>
+ internal static RequestCachePolicy DefaultCachePolicy = HttpWebRequest.DefaultCachePolicy;
+
+ /// <summary>
+ /// Gets or sets the cache that can be used for HTTP requests made during identifier discovery.
+ /// </summary>
+ internal static RequestCachePolicy IdentifierDiscoveryCachePolicy = new System.Net.Cache.HttpRequestCachePolicy(System.Net.Cache.HttpRequestCacheLevel.CacheIfAvailable);
+
internal delegate UntrustedWebResponse MockRequestResponse(Uri uri, byte[] body, string[] acceptTypes);
/// <summary>
/// Used in unit testing to mock HTTP responses to expected requests.
@@ -229,10 +240,10 @@ namespace DotNetOpenId {
}
internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes) {
- return Request(uri, body, acceptTypes, false);
+ return Request(uri, body, acceptTypes, false, DefaultCachePolicy);
}
- internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes, bool requireSsl) {
+ internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes, bool requireSsl, RequestCachePolicy cachePolicy) {
// Since we may 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.
// We COULD allow automatic redirect in the cases where HTTPS is not required,
@@ -240,7 +251,7 @@ namespace DotNetOpenId {
Uri originalRequestUri = uri;
int i;
for (i = 0; i < MaximumRedirections; i++) {
- UntrustedWebResponse response = RequestInternal(uri, body, acceptTypes, requireSsl, false, originalRequestUri);
+ UntrustedWebResponse response = RequestInternal(uri, body, acceptTypes, requireSsl, false, originalRequestUri, cachePolicy);
if (response.StatusCode == HttpStatusCode.MovedPermanently ||
response.StatusCode == HttpStatusCode.Redirect ||
response.StatusCode == HttpStatusCode.RedirectMethod ||
@@ -254,7 +265,7 @@ namespace DotNetOpenId {
}
static UntrustedWebResponse RequestInternal(Uri uri, byte[] body, string[] acceptTypes,
- bool requireSsl, bool avoidSendingExpect100Continue, Uri originalRequestUri) {
+ bool requireSsl, bool avoidSendingExpect100Continue, Uri originalRequestUri, RequestCachePolicy cachePolicy) {
if (uri == null) throw new ArgumentNullException("uri");
if (originalRequestUri == null) throw new ArgumentNullException("originalRequestUri");
if (!isUriAllowable(uri)) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
@@ -278,6 +289,7 @@ namespace DotNetOpenId {
request.ReadWriteTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
request.Timeout = (int)Timeout.TotalMilliseconds;
request.KeepAlive = false;
+ request.CachePolicy = cachePolicy;
request.UserAgent = UserAgentValue;
if (acceptTypes != null)
request.Accept = string.Join(",", acceptTypes);
@@ -313,7 +325,7 @@ namespace DotNetOpenId {
if (response != null) {
if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
if (!avoidSendingExpect100Continue) { // must only try this once more
- return RequestInternal(uri, body, acceptTypes, requireSsl, true, originalRequestUri);
+ return RequestInternal(uri, body, acceptTypes, requireSsl, true, originalRequestUri, cachePolicy);
}
}
return getResponse(originalRequestUri, request.RequestUri, response);
diff --git a/src/DotNetOpenId/Util.cs b/src/DotNetOpenId/Util.cs
index af00898..5671c6d 100644
--- a/src/DotNetOpenId/Util.cs
+++ b/src/DotNetOpenId/Util.cs
@@ -338,7 +338,7 @@ namespace DotNetOpenId {
// The characters to escape here are inspired by
// http://code.google.com/p/doctype/wiki/ArticleXSSInJavaScript
- static readonly Dictionary<string, string> javascriptStaticStringEscaping = new Dictionary<string,string> {
+ static readonly Dictionary<string, string> javascriptStaticStringEscaping = new Dictionary<string, string> {
{"\\", @"\\" }, // this WAS just above the & substitution but we moved it here to prevent double-escaping
{"\t", @"\t" },
{"\n", @"\n" },
@@ -403,6 +403,29 @@ namespace DotNetOpenId {
}
}
}
+ internal static bool Contains<T>(IEnumerable<T> sequence, Func<T, bool> predicate) {
+ foreach (T item in sequence) {
+ if (predicate(item)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ internal static IEnumerable<T> Cast<T>(System.Collections.IEnumerable sequence) {
+ foreach (object item in sequence) {
+ yield return (T)item;
+ }
+ }
+ internal static int Count(System.Collections.IEnumerable sequence) {
+ int count = 0;
+ foreach (object item in sequence) {
+ count++;
+ }
+
+ return count;
+ }
+
/// <summary>
/// Tests two sequences for same contents and ordering.
/// </summary>
@@ -413,7 +436,7 @@ namespace DotNetOpenId {
IEnumerator<T> iterator1 = sequence1.GetEnumerator();
IEnumerator<T> iterator2 = sequence2.GetEnumerator();
- bool movenext1 , movenext2;
+ bool movenext1, movenext2;
while (true) {
movenext1 = iterator1.MoveNext();
movenext2 = iterator2.MoveNext();
@@ -457,7 +480,7 @@ namespace DotNetOpenId {
foreach (T obj in l) {
// Prepare the string repersentation of the object
string objString = obj != null ? obj.ToString() : "<NULL>";
-
+
// Indent every line printed
objString = objString.Replace(Environment.NewLine, Environment.NewLine + "\t");
sb.Append("\t");
diff --git a/src/DotNetOpenId/Yadis/Yadis.cs b/src/DotNetOpenId/Yadis/Yadis.cs
index 37a58e4..01edf67 100644
--- a/src/DotNetOpenId/Yadis/Yadis.cs
+++ b/src/DotNetOpenId/Yadis/Yadis.cs
@@ -32,7 +32,8 @@ namespace DotNetOpenId.Yadis {
return null;
}
response = UntrustedWebRequest.Request(uri, null,
- new[] { ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds }, requireSsl);
+ new[] { ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds }, requireSsl,
+ UntrustedWebRequest.IdentifierDiscoveryCachePolicy);
if (response.StatusCode != System.Net.HttpStatusCode.OK) {
Logger.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response.StatusCode, response.StatusCode, uri);
return null;
@@ -62,7 +63,7 @@ namespace DotNetOpenId.Yadis {
}
if (url != null) {
if (!requireSsl || string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
- response2 = UntrustedWebRequest.Request(url, null, new[] { ContentTypes.Xrds }, requireSsl);
+ response2 = UntrustedWebRequest.Request(url, null, new[] { ContentTypes.Xrds }, requireSsl, UntrustedWebRequest.IdentifierDiscoveryCachePolicy);
if (response2.StatusCode != System.Net.HttpStatusCode.OK) {
return null;
}
diff --git a/src/specs/OpenIdAjaxTextBox.htm b/src/specs/OpenIdAjaxTextBox.htm
new file mode 100644
index 0000000..8c219c9
--- /dev/null
+++ b/src/specs/OpenIdAjaxTextBox.htm
@@ -0,0 +1,51 @@
+<!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>OpenIdAjaxTextBox specification</title>
+</head>
+<body>
+
+ <h1>OpenIdAjaxTextBox specification</h1>
+ <h2>Requirements</h2>
+ <ol>
+ <li>User supplied text value preserved across postbacks</li>
+ <li>Successful authentication results, if any, preserved across postbacks both in
+ the user agent and server contexts.</li>
+ <li>Asserting Provider&#39;s logo, where available, should appear in the place of the
+ OpenID logo when successful assertion is provided.</li>
+ <li>Identity assertion should be verified by the RP web server during the first
+ postback of that web page where a positive assertion is available.</li>
+ <li>Visual indication of when discovery and authentication are taking place.</li>
+ <li>Authentication to be attempted in asynchronous immediate mode with each
+ Provider authorized by the claimed identifier until an authentication is
+ achieved if possible.</li>
+ <li>If no OpenID endpoint will assert the user&#39;s identity in immediate mode, assist
+ the user in &quot;setup&quot; mode authentication with the preferred Provider using a
+ popup window.</li>
+ <li>In a single page visit, all user supplied identifiers entered into the box
+ resulting in discovery will have the discovery results saved in temporary memory&nbsp;
+ at the user agent, and will be reused rather than performing rediscovery if the
+ user re-enters a previously entered identifier.</li>
+ <li>In a single page visit, if the user enters multiple user supplied identifiers
+ that on discovery resolve to the same claimed identifier, any authentication
+ attempt made on that claimed identifier will be used for the other user supplied
+ identifiers that resolve to the same claimed identifier.&nbsp; For example, if
+ the user types &quot;blog.nerdbank.net&quot; and authenticates to it, then changes the
+ value to &quot;<a href="http://blog.nerdbank.net">http://blog.nerdbank.net</a>&quot; then
+ when discovery reveals this as the same identifier, the successful
+ authentication from &quot;blog.nerdbank.net&quot; will be applied to the new identifier
+ implicitly.</li>
+ </ol>
+ <h2>Non-requirements</h2>
+ <ol>
+ <li>Multiple authentication attempt logs preserved across postbacks</li>
+ <li></li>
+ </ol>
+
+ <h2>Design considerations</h2>
+ <p>A throttle should be placed to limit how many immediate requests are made to
+ Providers simultaneously, to avoid XRDS files with many Providers causing the
+ login control to initiate so many connections that the browser slows down.</p>
+
+</body>
+</html>
diff --git a/src/version.txt b/src/version.txt
index f4269b0..e70b452 100644
--- a/src/version.txt
+++ b/src/version.txt
@@ -1 +1 @@
-2.5.7
+2.6.0