diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2008-08-29 08:44:43 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2008-08-29 08:44:43 -0700 |
commit | 67102dc426e7ca90095b6aa8bee4a60b31117e7c (patch) | |
tree | 40dccebe135bf556b38ffe522b8834ad2709888d | |
parent | 190ba46c2003e5e2b9dc764a4c4c50db31954301 (diff) | |
download | DotNetOpenAuth-67102dc426e7ca90095b6aa8bee4a60b31117e7c.zip DotNetOpenAuth-67102dc426e7ca90095b6aa8bee4a60b31117e7c.tar.gz DotNetOpenAuth-67102dc426e7ca90095b6aa8bee4a60b31117e7c.tar.bz2 |
Added support for the Simple Registration extension to be seen in javascript for the ajax login control.
-rw-r--r-- | samples/RelyingPartyPortal/ajaxlogin.aspx | 3 | ||||
-rw-r--r-- | samples/RelyingPartyPortal/ajaxlogin.aspx.cs | 4 | ||||
-rw-r--r-- | src/DotNetOpenId/DotNetOpenId.csproj | 1 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/IClientScriptExtension.cs | 23 | ||||
-rw-r--r-- | src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs | 54 | ||||
-rw-r--r-- | src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs | 32 | ||||
-rw-r--r-- | src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs | 73 | ||||
-rw-r--r-- | src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs | 2 | ||||
-rw-r--r-- | src/DotNetOpenId/Strings.Designer.cs | 18 | ||||
-rw-r--r-- | src/DotNetOpenId/Strings.resx | 6 | ||||
-rw-r--r-- | src/DotNetOpenId/Util.cs | 35 |
11 files changed, 233 insertions, 18 deletions
diff --git a/samples/RelyingPartyPortal/ajaxlogin.aspx b/samples/RelyingPartyPortal/ajaxlogin.aspx index bea2768..9f7606b 100644 --- a/samples/RelyingPartyPortal/ajaxlogin.aspx +++ b/samples/RelyingPartyPortal/ajaxlogin.aspx @@ -14,7 +14,8 @@ <cc1:OpenIdAjaxTextBox ID="OpenIdAjaxTextBox1" runat="server"
OnLoggingIn="OpenIdAjaxTextBox1_LoggingIn"
OnLoggedIn="OpenIdAjaxTextBox1_LoggedIn"
- OnClientAssertionReceived="alert('Demonstration of page-injected javascript handling authentication:\nClaimed Identifier is ' + sender.getClaimedIdentifier())" />
+ OnClientAssertionReceived="alert('Demonstration of page-injected javascript handling authentication:\nClaimed Identifier is ' + sender.getClaimedIdentifier() + '\nEmail: ' + sender.sreg.email + '\nNickname: ' + sender.sreg.nickname)"
+ onunconfirmedpositiveassertion="OpenIdAjaxTextBox1_UnconfirmedPositiveAssertion" />
</td>
</tr>
<tr>
diff --git a/samples/RelyingPartyPortal/ajaxlogin.aspx.cs b/samples/RelyingPartyPortal/ajaxlogin.aspx.cs index bdd55b7..fcbc1df 100644 --- a/samples/RelyingPartyPortal/ajaxlogin.aspx.cs +++ b/samples/RelyingPartyPortal/ajaxlogin.aspx.cs @@ -37,5 +37,9 @@ namespace ConsumerPortal { protected void editComment_Click(object sender, EventArgs e) {
multiView.ActiveViewIndex = 0;
}
+
+ protected void OpenIdAjaxTextBox1_UnconfirmedPositiveAssertion(object sender, OpenIdEventArgs e) {
+ OpenIdAjaxTextBox1.RegisterClientScriptExtension(new ClaimsResponse(), "sreg");
+ }
}
}
diff --git a/src/DotNetOpenId/DotNetOpenId.csproj b/src/DotNetOpenId/DotNetOpenId.csproj index fca5d05..3548a71 100644 --- a/src/DotNetOpenId/DotNetOpenId.csproj +++ b/src/DotNetOpenId/DotNetOpenId.csproj @@ -64,6 +64,7 @@ <Compile Include="Configuration\WhiteBlackListCollection.cs" />
<Compile Include="Configuration\WhiteBlackListElement.cs" />
<Compile Include="DiffieHellmanUtil.cs" />
+ <Compile Include="Extensions\IClientScriptExtension.cs" />
<Compile Include="Provider\ProviderSecuritySettings.cs" />
<Compile Include="RelyingParty\AuthenticationResponseSnapshot.cs" />
<Compile Include="RelyingParty\OpenIdAjaxTextBox.cs" />
diff --git a/src/DotNetOpenId/Extensions/IClientScriptExtension.cs b/src/DotNetOpenId/Extensions/IClientScriptExtension.cs new file mode 100644 index 0000000..48d30e5 --- /dev/null +++ b/src/DotNetOpenId/Extensions/IClientScriptExtension.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId.Extensions {
+ public interface IClientScriptExtension : IExtension {
+ /// <summary>
+ /// Reads the extension information on an authentication response from the provider.
+ /// </summary>
+ /// <param name="fields">The fields belonging to the extension.</param>
+ /// <param name="response">The incoming OpenID response carrying the extension.</param>
+ /// <param name="typeUri">The actual extension TypeUri that was recognized in the message.</param>
+ /// <returns>
+ /// A Javascript snippet that when executed on the user agent returns an object with
+ /// the information deserialized from the extension response.
+ /// </returns>
+ /// <remarks>
+ /// This method is called <b>before</b> the signature on the assertion response has been
+ /// verified. Therefore all information in these fields should be assumed unreliable
+ /// and potentially falsified.
+ /// </remarks>
+ string InitializeJavascriptData(IDictionary<string, string> fields, IAuthenticationResponse response, string typeUri);
+ }
+}
diff --git a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs index 0203bce..5d31122 100644 --- a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs +++ b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs @@ -6,13 +6,12 @@ ********************************************************/
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Net.Mail;
-using DotNetOpenId.Extensions;
+using System.Text;
using System.Xml.Serialization;
using DotNetOpenId.RelyingParty;
-using DotNetOpenId.Provider;
-using System.Collections.Generic;
namespace DotNetOpenId.Extensions.SimpleRegistration
{
@@ -22,7 +21,7 @@ namespace DotNetOpenId.Extensions.SimpleRegistration /// authenticating user.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals"), Serializable()]
- public sealed class ClaimsResponse : IExtensionResponse
+ public sealed class ClaimsResponse : IExtensionResponse, IClientScriptExtension
{
string typeUriToUse;
@@ -201,6 +200,52 @@ namespace DotNetOpenId.Extensions.SimpleRegistration #endregion
+ #region IClientScriptExtension Members
+
+ string createAddFieldJS(string propertyName, string value) {
+ return string.Format(CultureInfo.InvariantCulture, "{0}: '{1}',",
+ propertyName, Util.MakeSafeJavascriptValue(value));
+ }
+
+ string IClientScriptExtension.InitializeJavascriptData(IDictionary<string, string> sreg, IAuthenticationResponse response, string typeUri) {
+ StringBuilder builder = new StringBuilder();
+ builder.Append("{ ");
+
+ string nickname, email, fullName, dob, genderString, postalCode, country, language, timeZone;
+ if (sreg.TryGetValue(Constants.nickname, out nickname)) {
+ builder.Append(createAddFieldJS(Constants.nickname, nickname));
+ }
+ if (sreg.TryGetValue(Constants.email, out email)) {
+ builder.Append(createAddFieldJS(Constants.email, email));
+ }
+ if (sreg.TryGetValue(Constants.fullname, out fullName)) {
+ builder.Append(createAddFieldJS(Constants.fullname, fullName));
+ }
+ if (sreg.TryGetValue(Constants.dob, out dob)) {
+ builder.Append(createAddFieldJS(Constants.dob, dob));
+ }
+ if (sreg.TryGetValue(Constants.gender, out genderString)) {
+ builder.Append(createAddFieldJS(Constants.gender, genderString));
+ }
+ if (sreg.TryGetValue(Constants.postcode, out postalCode)) {
+ builder.Append(createAddFieldJS(Constants.postcode, postalCode));
+ }
+ if (sreg.TryGetValue(Constants.country, out country)) {
+ builder.Append(createAddFieldJS(Constants.country, country));
+ }
+ if (sreg.TryGetValue(Constants.language, out language)) {
+ builder.Append(createAddFieldJS(Constants.language, language));
+ }
+ if (sreg.TryGetValue(Constants.timezone, out timeZone)) {
+ builder.Append(createAddFieldJS(Constants.timezone, timeZone));
+ }
+
+ builder.Append("}");
+ return builder.ToString();
+ }
+
+ #endregion
+
/// <summary>
/// Tests equality of two <see cref="ClaimsResponse"/> objects.
/// </summary>
@@ -240,6 +285,5 @@ namespace DotNetOpenId.Extensions.SimpleRegistration if (one == null ^ other == null) return false;
return one.Equals(other);
}
-
}
}
\ No newline at end of file diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs index ba1bab1..10a5312 100644 --- a/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs +++ b/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs @@ -112,6 +112,26 @@ namespace DotNetOpenId.RelyingParty { get { return new Uri(Util.GetRequiredArg(signedArguments, Provider.Protocol.openid.return_to)); }
}
+ internal string GetExtensionClientScript(IClientScriptExtension extension) {
+ var fields = IncomingExtensions.GetExtensionArguments(extension.TypeUri);
+ if (fields != null) {
+ // The extension was found using the preferred TypeUri.
+ return extension.InitializeJavascriptData(fields, this, extension.TypeUri);
+ } else {
+ // The extension may still be found using secondary TypeUris.
+ if (extension.AdditionalSupportedTypeUris != null) {
+ foreach (string typeUri in extension.AdditionalSupportedTypeUris) {
+ fields = IncomingExtensions.GetExtensionArguments(typeUri);
+ if (fields != null) {
+ // We found one of the older ones.
+ return extension.InitializeJavascriptData(fields, this, typeUri);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
bool getExtension(IExtensionResponse extension) {
var fields = IncomingExtensions.GetExtensionArguments(extension.TypeUri);
if (fields != null) {
@@ -153,7 +173,7 @@ namespace DotNetOpenId.RelyingParty { }
internal static AuthenticationResponse Parse(IDictionary<string, string> query,
- OpenIdRelyingParty relyingParty, Uri requestUrl) {
+ OpenIdRelyingParty relyingParty, Uri requestUrl, bool verifySignature) {
if (query == null) throw new ArgumentNullException("query");
if (requestUrl == null) throw new ArgumentNullException("requestUrl");
@@ -203,7 +223,7 @@ namespace DotNetOpenId.RelyingParty { // verified.
// For the error-handling and cancellation cases, the info does not have to
// be verified, so we'll use whichever one is available.
- return parseIdResResponse(query, tokenEndpoint, responseEndpoint, relyingParty, requestUrl);
+ return parseIdResResponse(query, tokenEndpoint, responseEndpoint, relyingParty, requestUrl, verifySignature);
} else {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue,
@@ -213,7 +233,7 @@ namespace DotNetOpenId.RelyingParty { static AuthenticationResponse parseIdResResponse(IDictionary<string, string> query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint,
- OpenIdRelyingParty relyingParty, Uri requestUrl) {
+ OpenIdRelyingParty relyingParty, Uri requestUrl, bool verifyMessageSignature) {
// Use responseEndpoint if it is available so we get the
// Claimed Identifer correct in the AuthenticationResponse.
ServiceEndpoint unverifiedEndpoint = responseEndpoint ?? tokenEndpoint;
@@ -226,8 +246,10 @@ namespace DotNetOpenId.RelyingParty { verifyReturnTo(query, unverifiedEndpoint, requestUrl);
verifyDiscoveredInfoMatchesAssertedInfo(relyingParty, query, tokenEndpoint, responseEndpoint);
- verifyNonceUnused(query, unverifiedEndpoint, relyingParty.Store);
- verifySignature(relyingParty, query, unverifiedEndpoint);
+ if (verifyMessageSignature) {
+ verifyNonceUnused(query, unverifiedEndpoint, relyingParty.Store);
+ verifySignature(relyingParty, query, unverifiedEndpoint);
+ }
return new AuthenticationResponse(AuthenticationStatus.Authenticated, unverifiedEndpoint, query);
}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs index d214892..e331e1c 100644 --- a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs +++ b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -1,10 +1,12 @@ using System;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
+using DotNetOpenId.Extensions;
[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, "text/javascript")]
[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedDotNetOpenIdLogoResourceName, "image/gif")]
@@ -333,6 +335,30 @@ namespace DotNetOpenId.RelyingParty { #endregion
+ Dictionary<IClientScriptExtension, string> clientScriptExtensions = new Dictionary<IClientScriptExtension, string>();
+ /// <summary>
+ /// Allows an OpenID extension to read data out of an unverified positive authentication assertion
+ /// and send it down to the client browser so that Javascript running on the page can perform
+ /// some preprocessing on the extension data.
+ /// </summary>
+ /// <param name="extension">The extension that will read data from the assertion.</param>
+ /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param>
+ public void RegisterClientScriptExtension(IClientScriptExtension extension, string propertyName) {
+ if (extension == null) throw new ArgumentNullException("extension");
+ if (String.IsNullOrEmpty(propertyName)) throw new ArgumentNullException("propertyName");
+ if (clientScriptExtensions.ContainsValue(propertyName)) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ClientScriptExtensionPropertyNameCollision, propertyName), "propertyName");
+ }
+ foreach (IClientScriptExtension ext in clientScriptExtensions.Keys) {
+ if (ext.TypeUri == extension.TypeUri) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ClientScriptExtensionTypeUriCollision, extension.TypeUri), "extension");
+ }
+ }
+ clientScriptExtensions.Add(extension, propertyName);
+ }
+
/// <summary>
/// Prepares the control for loading.
/// </summary>
@@ -418,9 +444,27 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }} private void reportDiscoveryResult() {
Logger.InfoFormat("AJAX (iframe) callback from OP: {0}", Page.Request.Url);
- callbackUserAgentMethod("dnoi_internal.openidAuthResult(document.URL)");
+ List<string> assignments = new List<string>();
+
+ OpenIdRelyingParty rp = new OpenIdRelyingParty();
+ 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) {
+ foreach (var pair in clientScriptExtensions) {
+ string js = authResponse.GetExtensionClientScript(pair.Key);
+ if (string.IsNullOrEmpty(js)) {
+ js = "null";
+ }
+ assignments.Add(pair.Value + " = " + js);
+ }
+ }
+
+ callbackUserAgentMethod("dnoi_internal.openidAuthResult(document.URL)", assignments.ToArray());
}
+ /// <summary>
+ /// Prepares to render the control.
+ /// </summary>
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
@@ -475,13 +519,30 @@ if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }} /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
/// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
private void callbackUserAgentMethod(string methodCall) {
+ callbackUserAgentMethod(methodCall, null);
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ /// <param name="preAssignments">An optional list of assignments to make to the input box object before placing the method call.</param>
+ private void callbackUserAgentMethod(string methodCall, string[] preAssignments) {
Logger.InfoFormat("Sending Javascript callback: {0}", methodCall);
+ Page.Response.Write(@"<html><body><script language='javascript'>
+ var objSrc = window.frameElement ? window.frameElement.openidBox : window.opener.waiting_openidBox;
+");
+ if (preAssignments != null) {
+ foreach (string assignment in preAssignments) {
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture, " objSrc.{0};{1}", assignment, Environment.NewLine));
+ }
+ }
Page.Response.Write(string.Format(CultureInfo.InvariantCulture,
- @"<html><body><script language='javascript'>
- var objSrc = window.frameElement ? window.frameElement.openidBox : window.opener.waiting_openidBox;
- objSrc.{0};
- if (!window.frameElement) {{ self.close(); }}
- </script></body></html>", methodCall));
+@" objSrc.{0};
+ if (!window.frameElement) {{ self.close(); }}
+</script></body></html>", methodCall));
Page.Response.End();
}
}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs index ef96ffd..ef08245 100644 --- a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs @@ -284,7 +284,7 @@ namespace DotNetOpenId.RelyingParty { get {
if (response == null && isAuthenticationResponseReady) {
try {
- response = AuthenticationResponse.Parse(query, this, request);
+ response = AuthenticationResponse.Parse(query, this, request, true);
} catch (OpenIdException ex) {
response = new FailedAuthenticationResponse(ex);
}
diff --git a/src/DotNetOpenId/Strings.Designer.cs b/src/DotNetOpenId/Strings.Designer.cs index 55b184d..fea5283 100644 --- a/src/DotNetOpenId/Strings.Designer.cs +++ b/src/DotNetOpenId/Strings.Designer.cs @@ -106,6 +106,24 @@ namespace DotNetOpenId { }
/// <summary>
+ /// Looks up a localized string similar to An extension with this property name ('{0}') has already been registered..
+ /// </summary>
+ internal static string ClientScriptExtensionPropertyNameCollision {
+ get {
+ return ResourceManager.GetString("ClientScriptExtensionPropertyNameCollision", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An extension with this extension type URI ('{0}') has already been registered..
+ /// </summary>
+ internal static string ClientScriptExtensionTypeUriCollision {
+ get {
+ return ResourceManager.GetString("ClientScriptExtensionTypeUriCollision", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to An authentication request has already been created using CreateRequest()..
/// </summary>
internal static string CreateRequestAlreadyCalled {
diff --git a/src/DotNetOpenId/Strings.resx b/src/DotNetOpenId/Strings.resx index d775691..ac6a50d 100644 --- a/src/DotNetOpenId/Strings.resx +++ b/src/DotNetOpenId/Strings.resx @@ -132,6 +132,12 @@ <data name="ClaimedIdentifierMustBeSetFirst" xml:space="preserve">
<value>The ClaimedIdentifier property must be set first.</value>
</data>
+ <data name="ClientScriptExtensionPropertyNameCollision" xml:space="preserve">
+ <value>An extension with this property name ('{0}') has already been registered.</value>
+ </data>
+ <data name="ClientScriptExtensionTypeUriCollision" xml:space="preserve">
+ <value>An extension with this extension type URI ('{0}') has already been registered.</value>
+ </data>
<data name="CreateRequestAlreadyCalled" xml:space="preserve">
<value>An authentication request has already been created using CreateRequest().</value>
</data>
diff --git a/src/DotNetOpenId/Util.cs b/src/DotNetOpenId/Util.cs index 64a1be9..6b0efaf 100644 --- a/src/DotNetOpenId/Util.cs +++ b/src/DotNetOpenId/Util.cs @@ -249,6 +249,41 @@ namespace DotNetOpenId { return true;
}
+ // The characters to escape here are inspired by
+ // http://code.google.com/p/doctype/wiki/ArticleXSSInJavaScript
+ static readonly Dictionary<string, string> javascriptEscaping = new Dictionary<string,string> {
+ {"\t", @"\t" },
+ {"\n", @"\n" },
+ {"\r", @"\r" },
+ {"\u0085", @"\u0085" },
+ {"\u2028", @"\u2028" },
+ {"\u2029", @"\u2029" },
+ {"'", @"\x27" },
+ {"\"", @"\x22" },
+ {"\\", @"\\" }, // perhaps move this to the top so that newline characters still end up as newline characters and not "\n" sequences
+ {"&", @"\x26" },
+ {"<", @"\x3c" },
+ {">", @"\x3e" },
+ {"=", @"\x3d" },
+ };
+
+ /// <summary>
+ /// Prepares what SHOULD be simply a string value for safe injection into Javascript
+ /// by using appropriate character escaping.
+ /// </summary>
+ /// <param name="value">The untrusted string value to be escaped to protected against XSS attacks.</param>
+ /// <returns>The escaped string.</returns>
+ public static string MakeSafeJavascriptValue(string value) {
+ if (value == null) return "null";
+ // We use a StringBuilder because we have potentially many replacements to do,
+ // and we don't want to create a new string for every intermediate replacement step.
+ StringBuilder builder = new StringBuilder(value);
+ foreach (var pair in javascriptEscaping) {
+ builder.Replace(pair.Key, pair.Value);
+ }
+ return builder.ToString();
+ }
+
internal delegate R Func<T, R>(T t);
/// <summary>
/// Scans a list for matches with some element of the OpenID protocol,
|