From 7af1c64b1ad3b841d872346918e65ec0b6d55082 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 24 Jan 2009 17:50:09 -0800 Subject: [PATCH] Added the OpenIdAjaxTextBox control. --- .../Messaging/HttpRequestInfoTests.cs | 6 + src/DotNetOpenAuth/DotNetOpenAuth.csproj | 9 + src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs | 1 + src/DotNetOpenAuth/Messaging/MessagingUtilities.cs | 16 + .../Extensions/IClientScriptExtensionResponse.cs | 33 + .../OpenId/OpenIdStrings.Designer.cs | 18 + src/DotNetOpenAuth/OpenId/OpenIdStrings.resx | 6 + .../OpenId/RelyingParty/AuthenticationRequest.cs | 13 + .../OpenId/RelyingParty/OpenIdAjaxTextBox.cs | 1248 ++++++++++++++++++++ .../OpenId/RelyingParty/OpenIdAjaxTextBox.js | 737 ++++++++++++ .../OpenId/RelyingParty/OpenIdRelyingParty.cs | 949 +++++++-------- .../OpenId/RelyingParty/OpenIdTextBox.cs | 2 +- .../RelyingParty/PositiveAuthenticationResponse.cs | 14 +- .../OpenId/RelyingParty/login_failure.png | Bin 0 -> 714 bytes .../OpenId/RelyingParty/login_success (lock).png | Bin 0 -> 571 bytes .../OpenId/RelyingParty/login_success.png | Bin 0 -> 464 bytes src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif | Bin 0 -> 725 bytes 17 files changed, 2583 insertions(+), 469 deletions(-) create mode 100644 src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs create mode 100644 src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs create mode 100644 src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js create mode 100644 src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.png create mode 100644 src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).png create mode 100644 src/DotNetOpenAuth/OpenId/RelyingParty/login_success.png create mode 100644 src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif diff --git a/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs b/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs index 5333f97..971495f 100644 --- a/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs +++ b/src/DotNetOpenAuth.Test/Messaging/HttpRequestInfoTests.cs @@ -12,6 +12,12 @@ namespace DotNetOpenAuth.Test.Messaging { [TestClass] public class HttpRequestInfoTests : TestBase { [TestMethod] + public void CtorDefault() { + HttpRequestInfo info = new HttpRequestInfo(); + Assert.AreEqual("GET", info.HttpMethod); + } + + [TestMethod] public void CtorRequest() { HttpRequest request = new HttpRequest("file", "http://someserver?a=b", "a=b"); ////request.Headers["headername"] = "headervalue"; // PlatformNotSupportedException prevents us mocking this up diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 71bc702..0a4d79d 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -196,6 +196,7 @@ + @@ -264,6 +265,7 @@ + @@ -357,6 +359,13 @@ + + + + + + + \ No newline at end of file diff --git a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs index e498c6f..cb446e7 100644 --- a/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs @@ -36,6 +36,7 @@ namespace DotNetOpenAuth.Messaging { /// Initializes a new instance of the class. /// internal HttpRequestInfo() { + this.HttpMethod = "GET"; } /// diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 18b06e1..6c40fba 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -70,6 +70,22 @@ namespace DotNetOpenAuth.Messaging { } /// + /// Gets the query or form data from the original request (before any URL rewriting has occurred.) + /// + /// A set of name=value pairs. + public static NameValueCollection GetQueryOrFormFromContext() { + ErrorUtilities.VerifyHttpContext(); + HttpRequest request = HttpContext.Current.Request; + NameValueCollection query; + if (request.RequestType == "GET") { + query = GetQueryFromContextNVC(); + } else { + query = request.Form; + } + return query; + } + + /// /// Strips any and all URI query parameters that start with some prefix. /// /// The URI that may have a query with parameters to remove. diff --git a/src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs b/src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs new file mode 100644 index 0000000..c84f507 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/Extensions/IClientScriptExtensionResponse.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Andrew Arnott. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions { + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// + /// An interface that OpenID extensions can implement to allow authentication response + /// messages with included extensions to be processed by Javascript on the user agent. + /// + public interface IClientScriptExtensionResponse : IExtensionMessage { + /// + /// Reads the extension information on an authentication response from the provider. + /// + /// The incoming OpenID response carrying the extension. + /// + /// A Javascript snippet that when executed on the user agent returns an object with + /// the information deserialized from the extension response. + /// + /// + /// This method is called before the signature on the assertion response has been + /// verified. Therefore all information in these fields should be assumed unreliable + /// and potentially falsified. + /// + string InitializeJavaScriptData(IProtocolMessageWithExtensions response); + } +} diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index 11af056..bd9559c 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -169,6 +169,24 @@ namespace DotNetOpenAuth.OpenId { } /// + /// Looks up a localized string similar to An extension with this property name ('{0}') has already been registered.. + /// + internal static string ClientScriptExtensionPropertyNameCollision { + get { + return ResourceManager.GetString("ClientScriptExtensionPropertyNameCollision", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The extension '{0}' has already been registered.. + /// + internal static string ClientScriptExtensionTypeCollision { + get { + return ResourceManager.GetString("ClientScriptExtensionTypeCollision", resourceCulture); + } + } + + /// /// Looks up a localized string similar to An authentication request has already been created using CreateRequest().. /// internal static string CreateRequestAlreadyCalled { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index 1791e5d..dab82bb 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -271,4 +271,10 @@ Discovered endpoint info: No OpenId url is provided. + + An extension with this property name ('{0}') has already been registered. + + + The extension '{0}' has already been registered. + \ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs index ce627dc..f76a5a4 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/AuthenticationRequest.cs @@ -156,6 +156,19 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { get { return this.endpoint.Protocol.Version; } } + #endregion + + /// + /// Gets or sets how an association may or should be created or used + /// in the formulation of the authentication request. + /// + internal AssociationPreference AssociationPreference { + get { return this.associationPreference; } + set { this.associationPreference = value; } + } + + #region IAuthenticationRequest methods + /// /// Makes a dictionary of key/value pairs available when the authentication is completed. /// diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs new file mode 100644 index 0000000..9bdf2f8 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.cs @@ -0,0 +1,1248 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Andrew Arnott. All rights reserved. +// +//----------------------------------------------------------------------- + +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, "text/javascript")] +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedDotNetOpenIdLogoResourceName, "image/gif")] +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName, "image/gif")] +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName, "image/png")] +[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName, "image/png")] + +#pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Web; + using System.Web.UI; + using System.Web.UI.WebControls; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Extensions; + + /// + /// An ASP.NET control that provides a minimal text box that is OpenID-aware and uses AJAX for + /// a premium login experience. + /// + [DefaultProperty("Text"), ValidationProperty("Text")] + [ToolboxData("<{0}:OpenIdAjaxTextBox runat=\"server\" />")] + public class OpenIdAjaxTextBox : WebControl, ICallbackEventHandler { + /// + /// The name of the manifest stream containing the OpenIdAjaxTextBox.js file. + /// + internal const string EmbeddedScriptResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdAjaxTextBox.js"; + + /// + /// The name of the manifest stream containing the dotnetopenid_16x16.gif file. + /// + internal const string EmbeddedDotNetOpenIdLogoResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.dotnetopenid_16x16.gif"; + + /// + /// The name of the manifest stream containing the spinner.gif file. + /// + internal const string EmbeddedSpinnerResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.spinner.gif"; + + /// + /// The name of the manifest stream containing the login_success.png file. + /// + internal const string EmbeddedLoginSuccessResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.login_success.png"; + + /// + /// The name of the manifest stream containing the login_failure.png file. + /// + internal const string EmbeddedLoginFailureResourceName = Util.DefaultNamespace + ".OpenId.RelyingParty.login_failure.png"; + + #region Property viewstate keys + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string ColumnsViewStateKey = "Columns"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string OnClientAssertionReceivedViewStateKey = "OnClientAssertionReceived"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string AuthenticationResponseViewStateKey = "AuthenticationResponse"; + + /// + /// The viewstate key to use for storing the value of the a successful authentication. + /// + private const string AuthDataViewStateKey = "AuthData"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string AuthenticatedAsToolTipViewStateKey = "AuthenticatedAsToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string AuthenticationSucceededToolTipViewStateKey = "AuthenticationSucceededToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string ReturnToUrlViewStateKey = "ReturnToUrl"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string RealmUrlViewStateKey = "RealmUrl"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string LogOnInProgressMessageViewStateKey = "BusyToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string AuthenticationFailedToolTipViewStateKey = "AuthenticationFailedToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string IdentifierRequiredMessageViewStateKey = "BusyToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string BusyToolTipViewStateKey = "BusyToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string LogOnTextViewStateKey = "LoginText"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string ThrottleViewStateKey = "Throttle"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string LogOnToolTipViewStateKey = "LoginToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string NameViewStateKey = "Name"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string TimeoutViewStateKey = "Timeout"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string TextViewStateKey = "Text"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string TabIndexViewStateKey = "TabIndex"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string RetryToolTipViewStateKey = "RetryToolTip"; + + /// + /// The viewstate key to use for storing the value of the property. + /// + private const string RetryTextViewStateKey = "RetryText"; + + #endregion + + #region Property defaults + + /// + /// The default value for the property. + /// + private const int ColumnsDefault = 40; + + /// + /// The default value for the property. + /// + private const string ReturnToUrlDefault = ""; + + /// + /// The default value for the property. + /// + private const string RealmUrlDefault = "~/"; + + /// + /// The default value for the property. + /// + private const string LogOnInProgressMessageDefault = "Please wait for login to complete."; + + /// + /// The default value for the property. + /// + private const string AuthenticationSucceededToolTipDefault = "Authenticated by {0}."; + + /// + /// The default value for the property. + /// + private const string AuthenticatedAsToolTipDefault = "Authenticated as {0}."; + + /// + /// The default value for the property. + /// + private const string AuthenticationFailedToolTipDefault = "Authentication failed."; + + /// + /// The default value for the property. + /// + private const int ThrottleDefault = 3; + + /// + /// The default value for the property. + /// + private const string LogOnTextDefault = "LOG IN"; + + /// + /// The default value for the property. + /// + private const string BusyToolTipDefault = "Discovering/authenticating"; + + /// + /// The default value for the property. + /// + private const string IdentifierRequiredMessageDefault = "Please correct errors in OpenID identifier and allow login to complete before submitting."; + + /// + /// The default value for the property. + /// + private const string NameDefault = "openid_identifier"; + + /// + /// Default value for property. + /// + private const short TabIndexDefault = 0; + + /// + /// The default value for the property. + /// + private const string RetryToolTipDefault = "Retry a failed identifier discovery."; + + /// + /// The default value for the property. + /// + private const string LogOnToolTipDefault = "Click here to log in using a pop-up window."; + + /// + /// The default value for the property. + /// + private const string RetryTextDefault = "RETRY"; + + #endregion + + /// + /// Tracks whether the text box should receive input focus when the page is rendered. + /// + private bool focusCalled; + + /// + /// The authentication response that just came in. + /// + private IAuthenticationResponse authenticationResponse; + + /// + /// A dictionary of extension response types and the javascript member + /// name to map them to on the user agent. + /// + private Dictionary clientScriptExtensions = new Dictionary(); + + /// + /// Stores the result of an AJAX discovery request while it is waiting + /// to be picked up by ASP.NET on the way down to the user agent. + /// + private string discoveryResult; + + #region Events + + /// + /// Fired when the user has typed in their identifier, discovery was successful + /// and a login attempt is about to begin. + /// + [Description("Fired when the user has typed in their identifier, discovery was successful and a login attempt is about to begin.")] + public event EventHandler LoggingIn; + + /// + /// Fired when a Provider sends back a positive assertion to this control, + /// but the authentication has not yet been verified. + /// + /// + /// No security critical decisions should be made within event handlers + /// for this event as the authenticity of the assertion has not been + /// verified yet. All security related code should go in the event handler + /// for the event. + /// + [Description("Fired when a Provider sends back a positive assertion to this control, but the authentication has not yet been verified.")] + public event EventHandler UnconfirmedPositiveAssertion; + + /// + /// Fired when authentication has completed successfully. + /// + [Description("Fired when authentication has completed successfully.")] + public event EventHandler LoggedIn; + + /// + /// Gets or sets the client-side script that executes when an authentication + /// assertion is received (but before it is verified). + /// + /// + /// In the context of the executing javascript set in this property, the + /// local variable sender is set to the openid_identifier input box + /// that is executing this code. + /// This variable has a getClaimedIdentifier() method that may be used to + /// identify the user who is being authenticated. + /// It is very important to note that when this code executes, + /// the authentication has not been verified and may have been spoofed. + /// No security-sensitive operations should take place in this javascript code. + /// The authentication is verified on the server by the time the + /// server-side event fires. + /// + [Description("Gets or sets the client-side script that executes when an authentication assertion is received (but before it is verified).")] + [Bindable(true), DefaultValue(""), Category("Behavior")] + public string OnClientAssertionReceived { + get { return this.ViewState[OnClientAssertionReceivedViewStateKey] as string; } + set { this.ViewState[OnClientAssertionReceivedViewStateKey] = value; } + } + + #endregion + + #region Properties + + /// + /// Gets the completed authentication response. + /// + public IAuthenticationResponse AuthenticationResponse { + get { + if (this.authenticationResponse == null) { + // We will either validate a new response and return a live AuthenticationResponse + // or we will try to deserialize a previous IAuthenticationResponse (snapshot) + // from viewstate and return that. + IAuthenticationResponse viewstateResponse = this.ViewState[AuthenticationResponseViewStateKey] as IAuthenticationResponse; + string viewstateAuthData = this.ViewState[AuthDataViewStateKey] as string; + string formAuthData = this.Page.Request.Form[this.OpenIdAuthDataFormKey]; + + // First see if there is fresh auth data to be processed into a response. + if (!string.IsNullOrEmpty(formAuthData) && !string.Equals(viewstateAuthData, formAuthData, StringComparison.Ordinal)) { + this.ViewState[AuthDataViewStateKey] = formAuthData; + + Uri authUri = new Uri(formAuthData); + HttpRequestInfo clientResponseInfo = new HttpRequestInfo { + Url = authUri, + }; + var rp = CreateRelyingParty(true); + this.authenticationResponse = rp.GetResponse(clientResponseInfo); + + // Save out the authentication response to viewstate so we can find it on + // a subsequent postback. + this.ViewState[AuthenticationResponseViewStateKey] = this.authenticationResponse; + } else { + this.authenticationResponse = viewstateResponse; + } + } + return this.authenticationResponse; + } + } + + /// + /// Gets or sets the value in the text field, completely unprocessed or normalized. + /// + [Bindable(true), DefaultValue(""), Category("Appearance")] + [Description("The value in the text field, completely unprocessed or normalized.")] + public string Text { + get { return (string)(this.ViewState[TextViewStateKey] ?? string.Empty); } + set { this.ViewState[TextViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the width of the text box in characters. + /// + [Bindable(true), Category("Appearance"), DefaultValue(ColumnsDefault)] + [Description("The width of the text box in characters.")] + public int Columns { + get { + return (int)(this.ViewState[ColumnsViewStateKey] ?? ColumnsDefault); + } + + set { + ErrorUtilities.VerifyArgumentInRange(value >= 0, "value"); + this.ViewState[ColumnsViewStateKey] = value; + } + } + + /// + /// Gets or sets the tab index of the text box control. Use 0 to omit an explicit tabindex. + /// + [Bindable(true), Category("Behavior"), DefaultValue(TabIndexDefault)] + [Description("The tab index of the text box control. Use 0 to omit an explicit tabindex.")] + public override short TabIndex { + get { return (short)(this.ViewState[TabIndexViewStateKey] ?? TabIndexDefault); } + set { this.ViewState[TabIndexViewStateKey] = value; } + } + + /// + /// Gets or sets the HTML name to assign to the text field. + /// + [Bindable(true), DefaultValue(NameDefault), Category("Misc")] + [Description("The HTML name to assign to the text field.")] + public string Name { + get { + return (string)(this.ViewState[NameViewStateKey] ?? NameDefault); + } + + set { + ErrorUtilities.VerifyNonZeroLength(value, "value"); + this.ViewState[NameViewStateKey] = value ?? string.Empty; + } + } + + /// + /// Gets or sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user. + /// + [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)(this.ViewState[TimeoutViewStateKey] ?? TimeoutDefault); + } + + set { + ErrorUtilities.VerifyArgumentInRange(value.TotalMilliseconds > 0, "value"); + this.ViewState[TimeoutViewStateKey] = value; + } + } + + /// + /// Gets or sets the maximum number of OpenID Providers to simultaneously try to authenticate with. + /// + [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)(this.ViewState[ThrottleViewStateKey] ?? ThrottleDefault); + } + + set { + ErrorUtilities.VerifyArgumentInRange(value > 0, "value"); + this.ViewState[ThrottleViewStateKey] = value; + } + } + + /// + /// Gets or sets the text that appears on the LOG IN button in cases where immediate (invisible) authentication fails. + /// + [Bindable(true), DefaultValue(LogOnTextDefault), Localizable(true), Category("Appearance")] + [Description("The text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")] + public string LogOnText { + get { + return (string)(this.ViewState[LogOnTextViewStateKey] ?? LogOnTextDefault); + } + + set { + ErrorUtilities.VerifyNonZeroLength(value, "value"); + this.ViewState[LogOnTextViewStateKey] = value ?? string.Empty; + } + } + + /// + /// Gets or sets the rool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails. + /// + [Bindable(true), DefaultValue(LogOnToolTipDefault), Localizable(true), Category("Appearance")] + [Description("The tool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")] + public string LogOnToolTip { + get { return (string)(this.ViewState[LogOnToolTipViewStateKey] ?? LogOnToolTipDefault); } + set { this.ViewState[LogOnToolTipViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the text that appears on the RETRY button in cases where authentication times out. + /// + [Bindable(true), DefaultValue(RetryTextDefault), Localizable(true), Category("Appearance")] + [Description("The text that appears on the RETRY button in cases where authentication times out.")] + public string RetryText { + get { + return (string)(this.ViewState[RetryTextViewStateKey] ?? RetryTextDefault); + } + + set { + ErrorUtilities.VerifyNonZeroLength(value, "value"); + this.ViewState[RetryTextViewStateKey] = value ?? string.Empty; + } + } + + /// + /// Gets or sets the tool tip text that appears on the RETRY button in cases where authentication times out. + /// + [Bindable(true), DefaultValue(RetryToolTipDefault), Localizable(true), Category("Appearance")] + [Description("The tool tip text that appears on the RETRY button in cases where authentication times out.")] + public string RetryToolTip { + get { return (string)(this.ViewState[RetryToolTipViewStateKey] ?? RetryToolTipDefault); } + set { this.ViewState[RetryToolTipViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the tool tip text that appears when authentication succeeds. + /// + [Bindable(true), DefaultValue(AuthenticationSucceededToolTipDefault), Localizable(true), Category("Appearance")] + [Description("The tool tip text that appears when authentication succeeds.")] + public string AuthenticationSucceededToolTip { + get { return (string)(this.ViewState[AuthenticationSucceededToolTipViewStateKey] ?? AuthenticationSucceededToolTipDefault); } + set { this.ViewState[AuthenticationSucceededToolTipViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the tool tip text that appears on the green checkmark when authentication succeeds. + /// + [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)(this.ViewState[AuthenticatedAsToolTipViewStateKey] ?? AuthenticatedAsToolTipDefault); } + set { this.ViewState[AuthenticatedAsToolTipViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the tool tip text that appears when authentication fails. + /// + [Bindable(true), DefaultValue(AuthenticationFailedToolTipDefault), Localizable(true), Category("Appearance")] + [Description("The tool tip text that appears when authentication fails.")] + public string AuthenticationFailedToolTip { + get { return (string)(this.ViewState[AuthenticationFailedToolTipViewStateKey] ?? AuthenticationFailedToolTipDefault); } + set { this.ViewState[AuthenticationFailedToolTipViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the tool tip text that appears over the text box when it is discovering and authenticating. + /// + [Bindable(true), DefaultValue(BusyToolTipDefault), Localizable(true), Category("Appearance")] + [Description("The tool tip text that appears over the text box when it is discovering and authenticating.")] + public string BusyToolTip { + get { return (string)(this.ViewState[BusyToolTipViewStateKey] ?? BusyToolTipDefault); } + set { this.ViewState[BusyToolTipViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the message that is displayed if a postback is about to occur before the identifier has been supplied. + /// + [Bindable(true), DefaultValue(IdentifierRequiredMessageDefault), Localizable(true), Category("Appearance")] + [Description("The message that is displayed if a postback is about to occur before the identifier has been supplied.")] + public string IdentifierRequiredMessage { + get { return (string)(this.ViewState[IdentifierRequiredMessageViewStateKey] ?? IdentifierRequiredMessageDefault); } + set { this.ViewState[IdentifierRequiredMessageViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the message that is displayed if a postback is attempted while login is in process. + /// + [Bindable(true), DefaultValue(LogOnInProgressMessageDefault), Localizable(true), Category("Appearance")] + [Description("The message that is displayed if a postback is attempted while login is in process.")] + public string LogOnInProgressMessage { + get { return (string)(this.ViewState[LogOnInProgressMessageViewStateKey] ?? LogOnInProgressMessageDefault); } + set { this.ViewState[LogOnInProgressMessageViewStateKey] = value ?? string.Empty; } + } + + /// + /// Gets or sets the OpenID of the relying party web site. + /// + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.Realm", Justification = "Using ctor for validation.")] + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Property grid on form designer only supports primitive types.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid on form designer only supports primitive types.")] + [Bindable(true)] + [Category("Behavior")] + [DefaultValue(RealmUrlDefault)] + [Description("The OpenID Realm of the relying party web site.")] + public string RealmUrl { + get { + return (string)(this.ViewState[RealmUrlViewStateKey] ?? RealmUrlDefault); + } + + set { + if (Page != null && !DesignMode) { + // Validate new value by trying to construct a Realm object based on it. + new Realm(OpenIdUtilities.GetResolvedRealm(Page, value)); // throws an exception on failure. + } else { + // We can't fully test it, but it should start with either ~/ or a protocol. + if (Regex.IsMatch(value, @"^https?://")) { + new Uri(value.Replace("*.", "")); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + this.ViewState[RealmUrlViewStateKey] = value; + } + } + + /// + /// Gets or sets the OpenID ReturnTo of the relying party web site. + /// + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Property grid on form designer only supports primitive types.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property grid on form designer only supports primitive types.")] + [Bindable(true)] + [Category("Behavior")] + [DefaultValue(ReturnToUrlDefault)] + [Description("The OpenID ReturnTo of the relying party web site.")] + public string ReturnToUrl { + get { + return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault); + } + + set { + if (Page != null && !DesignMode) { + // Validate new value by trying to construct a Uri based on it. + new Uri(MessagingUtilities.GetRequestUrlFromContext(), Page.ResolveUrl(value)); // throws an exception on failure. + } else { + // We can't fully test it, but it should start with either ~/ or a protocol. + if (Regex.IsMatch(value, @"^https?://")) { + new Uri(value); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + this.ViewState[ReturnToUrlViewStateKey] = value; + } + } + + #endregion + + #region Properties to hide + + /// + /// Gets or sets the foreground color (typically the color of the text) of the Web server control. + /// + /// + /// A that represents the foreground color of the control. The default is . + /// + [Obsolete, Browsable(false), Bindable(false)] + public override System.Drawing.Color ForeColor { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the background color of the Web server control. + /// + /// + /// A that represents the background color of the control. The default is , which indicates that this property is not set. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override System.Drawing.Color BackColor { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the border color of the Web control. + /// + /// + /// A that represents the border color of the control. The default is , which indicates that this property is not set. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override System.Drawing.Color BorderColor { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the border width of the Web server control. + /// + /// + /// A that represents the border width of a Web server control. The default value is , which indicates that this property is not set. + /// + /// + /// The specified border width is a negative value. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override Unit BorderWidth { + get { return Unit.Empty; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the border style of the Web server control. + /// + /// + /// One of the enumeration values. The default is NotSet. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override BorderStyle BorderStyle { + get { return BorderStyle.None; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets the font properties associated with the Web server control. + /// + /// + /// A that represents the font properties of the Web server control. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override FontInfo Font { + get { return null; } + } + + /// + /// Gets or sets the height of the Web server control. + /// + /// + /// A that represents the height of the control. The default is . + /// + /// + /// The height was set to a negative value. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override Unit Height { + get { return Unit.Empty; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the width of the Web server control. + /// + /// + /// A that represents the width of the control. The default is . + /// + /// + /// The width of the Web server control was set to a negative value. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override Unit Width { + get { return Unit.Empty; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the text displayed when the mouse pointer hovers over the Web server control. + /// + /// + /// The text displayed when the mouse pointer hovers over the Web server control. The default is . + /// + [Obsolete, Browsable(false), Bindable(false)] + public override string ToolTip { + get { return string.Empty; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets the skin to apply to the control. + /// + /// + /// The name of the skin to apply to the control. The default is . + /// + /// + /// The skin specified in the property does not exist in the theme. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override string SkinID { + get { return string.Empty; } + set { throw new NotSupportedException(); } + } + + /// + /// Gets or sets a value indicating whether themes apply to this control. + /// + /// true to use themes; otherwise, false. The default is false. + /// + [Obsolete, Browsable(false), Bindable(false)] + public override bool EnableTheming { + get { return false; } + set { throw new NotSupportedException(); } + } + + #endregion + + /// + /// Gets the default value for the property. + /// + /// 8 seconds; or eternity if the debugger is attached. + private static 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); + } + } + } + + /// + /// Gets the name of the open id auth data form key. + /// + /// A concatenation of and "_openidAuthData". + private string OpenIdAuthDataFormKey { + get { return this.Name + "_openidAuthData"; } + } + + /// + /// Places focus on the text box when the page is rendered on the browser. + /// + public override void Focus() { + // we don't emit the code to focus the control immediately, in case the control + // is never rendered to the page because its Visible property is false or that + // of any of its parent containers. + this.focusCalled = true; + } + + /// + /// 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. + /// + /// The extension response type that will read data from the assertion. + /// The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg + /// + /// This method should be called from the event handler. + /// + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")] + public void RegisterClientScriptExtension(string propertyName) where T : IClientScriptExtensionResponse { + ErrorUtilities.VerifyNonZeroLength(propertyName, "propertyName"); + ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName); + foreach (var ext in this.clientScriptExtensions.Keys) { + ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName); + } + this.clientScriptExtensions.Add(typeof(T), propertyName); + } + + #region ICallbackEventHandler Members + + /// + /// Returns the result of discovery on some Identifier passed to . + /// + /// The result of the callback. + /// A whitespace delimited list of URLs that can be used to initiate authentication. + string ICallbackEventHandler.GetCallbackResult() { + this.Page.Response.ContentType = "text/javascript"; + return this.discoveryResult; + } + + /// + /// Performs discovery on some OpenID Identifier. Called directly from the user agent via + /// AJAX callback mechanisms. + /// + /// The identifier to perform discovery on. + void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) { + string userSuppliedIdentifier = eventArgument; + + ErrorUtilities.VerifyNonZeroLength(userSuppliedIdentifier, "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 requests = this.CreateRequests(userSuppliedIdentifier, true); + if (requests.Count > 0) { + discoveryResultBuilder.AppendFormat("claimedIdentifier: {0},", Util.GetSafeJavascriptValue(requests[0].ClaimedIdentifier)); + discoveryResultBuilder.Append("requests: ["); + foreach (IAuthenticationRequest request in requests) { + this.OnLoggingIn(request); + discoveryResultBuilder.Append("{"); + discoveryResultBuilder.AppendFormat("endpoint: {0},", Util.GetSafeJavascriptValue(request.Provider.Uri.AbsoluteUri)); + request.Mode = AuthenticationRequestMode.Immediate; + UserAgentResponse response = request.RedirectingResponse; + discoveryResultBuilder.AppendFormat("immediate: {0},", Util.GetSafeJavascriptValue(response.DirectUriRequest.AbsoluteUri)); + request.Mode = AuthenticationRequestMode.Setup; + response = request.RedirectingResponse; + discoveryResultBuilder.AppendFormat("setup: {0}", Util.GetSafeJavascriptValue(response.DirectUriRequest.AbsoluteUri)); + discoveryResultBuilder.Append("},"); + } + discoveryResultBuilder.Length -= 1; // trim off last comma + discoveryResultBuilder.Append("]"); + } else { + discoveryResultBuilder.Append("requests: new Array(),"); + discoveryResultBuilder.AppendFormat("error: {0}", Util.GetSafeJavascriptValue(OpenIdStrings.OpenIdEndpointNotFound)); + } + } catch (ProtocolException ex) { + discoveryResultBuilder.Append("requests: new Array(),"); + discoveryResultBuilder.AppendFormat("error: {0}", Util.GetSafeJavascriptValue(ex.Message)); + } + discoveryResultBuilder.Append("}"); + this.discoveryResult = discoveryResultBuilder.ToString(); + } + + #endregion + + /// + /// Fires the event. + /// + /// The request. + protected virtual void OnLoggingIn(IAuthenticationRequest request) { + var loggingIn = this.LoggingIn; + if (loggingIn != null) { + loggingIn(this, new OpenIdEventArgs(request)); + } + } + + /// + /// Fires the event. + /// + protected virtual void OnUnconfirmedPositiveAssertion() { + var unconfirmedPositiveAssertion = this.UnconfirmedPositiveAssertion; + if (unconfirmedPositiveAssertion != null) { + unconfirmedPositiveAssertion(this, null); + } + } + + /// + /// Fires the event. + /// + /// The response. + protected virtual void OnLoggedIn(IAuthenticationResponse response) { + var loggedIn = this.LoggedIn; + if (loggedIn != null) { + loggedIn(this, new OpenIdEventArgs(response)); + } + } + + /// + /// Prepares the control for loading. + /// + /// The object that contains the event data. + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + + if (this.Page.IsPostBack) { + // If the control was temporarily hidden, it won't be in the Form data, + // and we'll just implicitly keep the last Text setting. + if (this.Page.Request.Form[this.Name] != null) { + this.Text = this.Page.Request.Form[this.Name]; + } + + // If there is a response, and it is fresh (live object, not a snapshot object)... + if (this.AuthenticationResponse != null && this.AuthenticationResponse.Status == AuthenticationStatus.Authenticated) { + this.OnLoggedIn(this.AuthenticationResponse); + } + } else { + NameValueCollection query = MessagingUtilities.GetQueryOrFormFromContext(); + string userSuppliedIdentifier = query["dotnetopenid.userSuppliedIdentifier"]; + if (!string.IsNullOrEmpty(userSuppliedIdentifier) && query["dotnetopenid.phase"] == "2") { + this.ReportAuthenticationResult(); + } + } + } + + /// + /// Prepares to render the control. + /// + /// An object that contains the event data. + protected override void OnPreRender(EventArgs e) { + base.OnPreRender(e); + + this.PrepareClientJavascript(); + } + + /// + /// Renders the control. + /// + /// The object that receives the control content. + protected override void Render(System.Web.UI.HtmlTextWriter writer) { + // We surround the textbox with a span so that the .js file can inject a + // login button within the text box with easy placement. + writer.WriteBeginTag("span"); + writer.WriteAttribute("class", this.CssClass); + writer.Write(" style='"); + writer.WriteStyleAttribute("position", "relative"); + writer.WriteStyleAttribute("font-size", "16px"); + writer.Write("'>"); + + writer.WriteBeginTag("input"); + writer.WriteAttribute("name", this.Name); + writer.WriteAttribute("id", this.ClientID); + writer.WriteAttribute("value", this.Text, true); + writer.WriteAttribute("size", this.Columns.ToString(CultureInfo.InvariantCulture)); + if (this.TabIndex > 0) { + writer.WriteAttribute("tabindex", this.TabIndex.ToString(CultureInfo.InvariantCulture)); + } + if (!this.Enabled) { + writer.WriteAttribute("disabled", "true"); + } + if (!string.IsNullOrEmpty(this.CssClass)) { + writer.WriteAttribute("class", this.CssClass); + } + writer.Write(" style='"); + writer.WriteStyleAttribute("padding-left", "18px"); + writer.WriteStyleAttribute("border-style", "solid"); + writer.WriteStyleAttribute("border-width", "1px"); + writer.WriteStyleAttribute("border-color", "lightgray"); + writer.Write("'"); + 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 = this.ViewState[AuthDataViewStateKey] as string; + if (!string.IsNullOrEmpty(viewstateAuthData)) { + writer.WriteBeginTag("input"); + writer.WriteAttribute("type", "hidden"); + writer.WriteAttribute("name", this.OpenIdAuthDataFormKey); + writer.WriteAttribute("value", viewstateAuthData, true); + writer.Write(" />"); + } + } + + /// + /// Filters a sequence of OP endpoints so that an OP hostname only appears once in the list. + /// + /// The authentication requests against those OP endpoints. + /// The filtered list. + private static List RemoveDuplicateEndpoints(List requests) { + var filteredRequests = new List(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 (!filteredRequests.Any(req => string.Equals(req.Provider.Uri.Host, request.Provider.Uri.Host, StringComparison.OrdinalIgnoreCase))) { + filteredRequests.Add(request); + } + } + + return filteredRequests; + } + + /// + /// Creates the relying party. + /// + /// + /// A value indicating whether message protections should be applied to the processed messages. + /// Use false to postpone verification to a later time without invalidating nonces. + /// + /// The newly instantiated relying party. + private static OpenIdRelyingParty CreateRelyingParty(bool verifySignature) { + return verifySignature ? new OpenIdRelyingParty() : OpenIdRelyingParty.CreateNonVerifying(); + } + + /// + /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, + /// and closes the calling popup window if applicable. + /// + /// The method to call on the OpenIdAjaxTextBox, including + /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method. + private void CallbackUserAgentMethod(string methodCall) { + this.CallbackUserAgentMethod(methodCall, null); + } + + /// + /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox, + /// and closes the calling popup window if applicable. + /// + /// The method to call on the OpenIdAjaxTextBox, including + /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method. + /// An optional list of assignments to make to the input box object before placing the method call. + private void CallbackUserAgentMethod(string methodCall, string[] preAssignments) { + Logger.InfoFormat("Sending Javascript callback: {0}", methodCall); + Page.Response.Write(@""; + Page.Response.Write(string.Format(CultureInfo.InvariantCulture, htmlFormat, methodCall)); + Page.Response.End(); + } + + /// + /// Assembles the javascript to send to the client and registers it with ASP.NET for transmission. + /// + 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. + this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdAjaxTextBox), EmbeddedScriptResourceName); + + // Call into the .js file with initialization information. + StringBuilder startupScript = new StringBuilder(); + startupScript.AppendLine(""); + + Page.ClientScript.RegisterStartupScript(this.GetType(), "ajaxstartup", startupScript.ToString()); + string htmlFormat = @" +var openidbox = document.getElementsByName('{0}')[0]; +if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }} +"; + Page.ClientScript.RegisterOnSubmitStatement( + this.GetType(), + "loginvalidation", + string.Format(CultureInfo.InvariantCulture, htmlFormat, this.Name)); + } + + /// + /// Creates the authentication requests for a given user-supplied Identifier. + /// + /// The user supplied identifier. + /// A value indicating whether the authentication + /// requests should be initialized for use in invisible iframes for background authentication. + /// The list of authentication requests, any one of which may be + /// used to determine the user's control of the . + private List CreateRequests(string userSuppliedIdentifier, bool immediate) { + var requests = new List(); + + OpenIdRelyingParty rp = CreateRelyingParty(true); + + // Resolve the trust root, and swap out the scheme and port if necessary to match the + // return_to URL, since this match is required by OpenId, and the consumer app + // may be using HTTP at some times and HTTPS at others. + UriBuilder realm = OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl); + realm.Scheme = Page.Request.Url.Scheme; + realm.Port = Page.Request.Url.Port; + + // Initiate openid request + // We use TryParse here to avoid throwing an exception which + // might slip through our validator control if it is disabled. + Realm typedRealm = new Realm(realm); + if (string.IsNullOrEmpty(this.ReturnToUrl)) { + requests.AddRange(rp.CreateRequests(userSuppliedIdentifier, typedRealm)); + } else { + Uri returnTo = new Uri(MessagingUtilities.GetRequestUrlFromContext(), this.ReturnToUrl); + requests.AddRange(rp.CreateRequests(userSuppliedIdentifier, typedRealm, returnTo)); + } + + // 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); + + // Configure each generated request. + 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 (immediate) { + req.Mode = AuthenticationRequestMode.Immediate; + ((AuthenticationRequest)req).AssociationPreference = AssociationPreference.IfAlreadyEstablished; + } + } + + return requests; + } + + /// + /// Notifies the user agent via an AJAX response of a completed authentication attempt. + /// + private void ReportAuthenticationResult() { + Logger.InfoFormat("AJAX (iframe) callback from OP: {0}", this.Page.Request.Url); + List assignments = new List(); + + OpenIdRelyingParty rp = CreateRelyingParty(false); + var f = HttpUtility.ParseQueryString(this.Page.Request.Url.Query).ToDictionary(); + var authResponse = rp.GetResponse(); + if (authResponse.Status == AuthenticationStatus.Authenticated) { + this.OnUnconfirmedPositiveAssertion(); + foreach (var pair in this.clientScriptExtensions) { + IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key); + var positiveResponse = (PositiveAuthenticationResponse)authResponse; + string js = extension.InitializeJavaScriptData(positiveResponse.Response); + if (string.IsNullOrEmpty(js)) { + js = "null"; + } + assignments.Add(pair.Value + " = " + js); + } + } + + this.CallbackUserAgentMethod("dnoi_internal.processAuthorizationResult(document.URL)", assignments.ToArray()); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js new file mode 100644 index 0000000..53f6d43 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdAjaxTextBox.js @@ -0,0 +1,737 @@ +// 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 + +function trace(msg) { + if (window.openid_trace) { + 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); + } +} + +/// Removes a given element from the array. +/// True if the element was in the array, or false if it was not found. +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, + throttle, timeout, assertionReceivedCode, + loginButtonText, loginButtonToolTip, retryButtonText, retryButtonToolTip, busyToolTip, + identifierRequiredMessage, loginInProgressMessage, + authenticatedByToolTip, authenticatedAsToolTip, authenticationFailedToolTip, + discoverCallback, discoveryFailedCallback) { + box.dnoi_internal = new Object(); + if (assertionReceivedCode) { + box.dnoi_internal.onauthenticated = function(sender, e) { eval(assertionReceivedCode); } + } + + 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; + + /// Called to queue up some work that will use an iframe as soon as it is available. + /// + /// 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. + /// + 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); + } + }; + + /// Clears the job queue and immediately closes all iframes. + this.cancelAllWork = function() { + trace('Canceling all open and pending iframes.'); + while (this.queuedWork.pop()); + this.closeFrames(); + }; + + /// An event fired when a frame is closing. + 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 != null ? tooltip : ''; + button.onclick = onclick; + button.style.visibility = 'hidden'; + button.style.position = 'absolute'; + button.style.padding = "0px"; + button.style.fontSize = '8px'; + button.style.top = "1px"; + button.style.bottom = "1px"; + button.style.right = "2px"; + box.parentNode.appendChild(button); + return button; + } + + box.dnoi_internal.constructIcon = function(imageUrl, tooltip, rightSide, visible, height) { + var icon = document.createElement('img'); + icon.src = imageUrl; + icon.title = tooltip != null ? tooltip : ''; + icon.originalTitle = icon.title; + if (!visible) { + icon.style.visibility = 'hidden'; + } + icon.style.position = 'absolute'; + icon.style.top = "2px"; + icon.style.bottom = "2px"; // for FireFox (and IE7, I think) + if (height) { + icon.style.height = height; // for Chrome and IE8 + } + if (rightSide) { + icon.style.right = "2px"; + } else { + icon.style.left = "2px"; + } + box.parentNode.appendChild(icon); + return icon; + } + + box.dnoi_internal.prefetchImage = function(imageUrl) { + var img = document.createElement('img'); + img.src = imageUrl; + img.style.display = 'none'; + box.parentNode.appendChild(img); + 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() { + 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.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('', 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, 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, 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.loginButton.style.visibility = 'hidden'; + box.dnoi_internal.retryButton.style.visibility = 'hidden'; + 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 = ''; + 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") { + var opLogo = box.dnoi_internal.deriveOPFavIcon(); + if (opLogo) { + box.dnoi_internal.op_logo.src = opLogo; + box.dnoi_internal.op_logo.style.visibility = 'visible'; + } else { + box.dnoi_internal.openid_logo.style.visibility = 'visible'; + } + box.dnoi_internal.loginButton.style.visibility = 'visible'; + box.dnoi_internal.claimedIdentifier = null; + window.status = "Authentication requires setup."; + } else if (state == "failed") { + box.dnoi_internal.openid_logo.style.visibility = 'visible'; + //box.dnoi_internal.failure_icon.style.visibility = 'visible'; + box.dnoi_internal.retryButton.style.visibility = 'visible'; + box.dnoi_internal.claimedIdentifier = null; + window.status = authenticationFailedToolTip; + box.title = authenticationFailedToolTip; + } else if (state = '' || state == null) { + box.dnoi_internal.openid_logo.style.visibility = 'visible'; + box.title = ''; + box.dnoi_internal.claimedIdentifier = null; + window.status = null; + } else { + box.dnoi_internal.claimedIdentifier = null; + trace('unrecognized state ' + state); + } + } + + box.dnoi_internal.isBusy = function() { + 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() { + return box.dnoi_internal.getUserSuppliedIdentifierResults().findSuccessfulRequest() != null; + } + + box.dnoi_internal.onSubmit = function() { + 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 || !box.dnoi_internal.canAttemptLogin()) { + alert(identifierRequiredMessage); + } else { + // The user hasn't clicked "Login" yet. We'll click login for him, + // after leaving a note for ourselves to automatically click submit + // when login is complete. + box.dnoi_internal.submitPending = box.dnoi_internal.submitButtonJustClicked; + if (box.dnoi_internal.submitPending == null) { + box.dnoi_internal.submitPending = true; + } + box.dnoi_internal.loginButton.onclick(); + return false; // abort submit for now + } + } else { + return true; + } + } + return false; + } + return true; + }; + + /// + /// 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. + /// + box.dnoi_internal.setLastSubmitButtonClicked = function(evt) { + var button; + if (evt.target) { + button = evt.target; + } else { + button = evt.srcElement; + } + + box.dnoi_internal.submitButtonJustClicked = button; + }; + + // 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); + } + } + } + + /// + /// Returns the URL of the authenticating OP's logo so it can be displayed to the user. + /// + box.dnoi_internal.deriveOPFavIcon = function() { + 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; + var favicon = opUri.getAuthority() + "/favicon.ico"; + return favicon; + }; + + 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); + } + }; + + 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 + + this.host = self.endpoint.getHost(); + + 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'); + }; + }; + + /***************************************** + * Flow + *****************************************/ + + /// Called to initiate discovery on some identifier. + 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); + }; + + /// Callback that is invoked when discovery fails. + box.dnoi_internal.discoveryFailed = function(message, identifier) { + box.dnoi_internal.setVisualCue('failed'); + if (message) { box.title = message; } + } + + /// Callback that is invoked when discovery results are available. + /// The JSON object containing the OpenID auth requests. + /// The identifier that discovery was performed on. + 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(); + } + } + + /// Invoked by RP web server when an authentication has completed. + /// The duty of this method is to distribute the notification to the appropriate tracking object. + box.dnoi_internal.processAuthorizationResult = function(resultUrl) { + self.waiting_openidBox = null; + //trace('processAuthorizationResult ' + resultUrl); + var resultUri = new Uri(resultUrl); + + // 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; + } + 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 = discoveryInfo.claimedIdentifier; + box.dnoi_internal.setVisualCue('authenticated', tracker.endpoint, discoveryInfo.claimedIdentifier); + if (box.dnoi_internal.onauthenticated) { + box.dnoi_internal.onauthenticated(box); + } + if (box.dnoi_internal.submitPending) { + // We submit the form BEFORE resetting the submitPending so + // the submit handler knows we've already tried this route. + if (box.dnoi_internal.submitPending == true) { + box.parentForm.submit(); + } else { + box.dnoi_internal.submitPending.click(); + } + } + } else { + tracker.authFailed(); + } + + box.dnoi_internal.submitPending = null; + }; + + function isAuthSuccessful(resultUri) { + if (isOpenID2Response(resultUri)) { + return resultUri.getQueryArgValue("openid.mode") == "id_res"; + } else { + return resultUri.getQueryArgValue("openid.mode") == "id_res" && !resultUri.containsQueryArg("openid.user_setup_url"); + } + }; + + function isOpenID2Response(resultUri) { + return resultUri.containsQueryArg("openid.ns"); + }; + + box.onblur = function(event) { + var discoveryInfo = box.dnoi_internal.authenticationRequests[box.value]; + if (discoveryInfo == null) { + if (box.value.length > 0) { + box.dnoi_internal.performDiscovery(box.value); + } else { + box.dnoi_internal.setVisualCue(); + } + } else { + if ((priorSuccess = discoveryInfo.findSuccessfulRequest())) { + box.dnoi_internal.setVisualCue('authenticated', priorSuccess.endpoint, discoveryInfo.claimedIdentifier); + } else { + discoveryInfo.tryImmediate(); + } + } + return true; + }; + box.onkeyup = function(event) { + 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) { + this.originalUri = url; + + this.toString = function() { + return this.originalUri; + }; + + this.getAuthority = function() { + var authority = this.getScheme() + "://" + this.getHost(); + return authority; + } + + this.getHost = function() { + var hostStartIdx = this.originalUri.indexOf("://") + 3; + var hostEndIndex = this.originalUri.indexOf("/", hostStartIdx); + if (hostEndIndex < 0) hostEndIndex = this.originalUri.length; + var host = this.originalUri.substr(hostStartIdx, hostEndIndex - hostStartIdx); + return host; + } + + this.getScheme = function() { + var schemeStartIdx = this.indexOf("://"); + return this.originalUri.substr(this.originalUri, schemeStartIdx); + } + + this.trimFragment = function() { + var hashmark = this.originalUri.indexOf('#'); + if (hashmark >= 0) { + return new Uri(this.originalUri.substr(0, hashmark)); + } + return this; + }; + + this.appendQueryVariable = function(name, value) { + var pair = encodeURI(name) + "=" + encodeURI(value); + if (this.originalUri.indexOf('?') >= 0) { + this.originalUri = this.originalUri + "&" + pair; + } else { + this.originalUri = this.originalUri + "?" + pair; + } + }; + + function KeyValuePair(key, value) { + this.key = key; + this.value = value; + }; + + this.Pairs = new Array(); + + var queryBeginsAt = this.originalUri.indexOf('?'); + if (queryBeginsAt >= 0) { + this.queryString = url.substr(queryBeginsAt + 1); + var queryStringPairs = this.queryString.split('&'); + + for (var i = 0; i < queryStringPairs.length; i++) { + var pair = queryStringPairs[i].split('='); + this.Pairs.push(new KeyValuePair(unescape(pair[0]), unescape(pair[1]))) + } + }; + + this.getQueryArgValue = function(key) { + for (var i = 0; i < this.Pairs.length; i++) { + if (this.Pairs[i].key == key) { + return this.Pairs[i].value; + } + } + }; + + this.containsQueryArg = function(key) { + return this.getQueryArgValue(key); + }; + + this.indexOf = function(args) { + return this.originalUri.indexOf(args); + }; + + return this; +}; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs index 4678137..1c57605 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs @@ -1,467 +1,482 @@ -//----------------------------------------------------------------------- -// -// Copyright (c) Andrew Arnott. All rights reserved. -// -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.OpenId.RelyingParty { - using System; - using System.Collections.Generic; - using System.Collections.Specialized; - using System.ComponentModel; - using System.Linq; - using System.Web; - using DotNetOpenAuth.Configuration; - using DotNetOpenAuth.Messaging; - using DotNetOpenAuth.Messaging.Bindings; - using DotNetOpenAuth.OpenId.ChannelElements; - using DotNetOpenAuth.OpenId.Messages; - - /// - /// A delegate that decides whether a given OpenID Provider endpoint may be - /// considered for authenticating a user. - /// - /// The endpoint for consideration. - /// - /// True if the endpoint should be considered. - /// False to remove it from the pool of acceptable providers. - /// - public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint); - - /// - /// Provides the programmatic facilities to act as an OpenId consumer. - /// - public sealed class OpenIdRelyingParty { - /// - /// The name of the key to use in the HttpApplication cache to store the - /// instance of to use. - /// - private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.ApplicationStore"; - - /// - /// Backing field for the property. - /// - private RelyingPartySecuritySettings securitySettings; - - /// - /// Backing store for the property. - /// - private Comparison endpointOrder = DefaultEndpointOrder; - - /// - /// Backing field for the property. - /// - private Channel channel; - - /// - /// Initializes a new instance of the class. - /// - public OpenIdRelyingParty() - : this(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) { - } - - /// - /// Initializes a new instance of the class. - /// - /// The application store. If null, the relying party will always operate in "dumb mode". - public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore) - : this(applicationStore, applicationStore, applicationStore) { - } - - /// - /// Initializes a new instance of the class. - /// - /// The association store. If null, the relying party will always operate in "dumb mode". - /// The nonce store to use. If null, the relying party will always operate in "dumb mode". - /// The secret store to use. If null, the relying party will always operate in "dumb mode". - private OpenIdRelyingParty(IAssociationStore associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) { - // If we are a smart-mode RP (supporting associations), then we MUST also be - // capable of storing nonces to prevent replay attacks. - // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays. - ErrorUtilities.VerifyArgument(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore); - - this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings(); - - // Without a nonce store, we must rely on the Provider to protect against - // replay attacks. But only 2.0+ Providers can be expected to provide - // replay protection. - if (nonceStore == null) { - this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; - } - - this.channel = new OpenIdChannel(associationStore, nonceStore, secretStore, this.SecuritySettings); - this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings); - } - - /// - /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority - /// attribute to determine order. - /// - /// - /// Endpoints lacking any priority value are sorted to the end of the list. - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static Comparison DefaultEndpointOrder { - get { return ServiceEndpoint.EndpointOrder; } - } - - /// - /// Gets the standard state storage mechanism that uses ASP.NET's - /// HttpApplication state dictionary to store associations and nonces. - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public static IRelyingPartyApplicationStore HttpApplicationStore { - get { - HttpContext context = HttpContext.Current; - ErrorUtilities.VerifyOperation(context != null, OpenIdStrings.StoreRequiredWhenNoHttpContextAvailable, typeof(IRelyingPartyApplicationStore).Name); - var store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]; - if (store == null) { - context.Application.Lock(); - try { - if ((store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]) == null) { - context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore(); - } - } finally { - context.Application.UnLock(); - } - } - - return store; - } - } - - /// - /// Gets or sets the channel to use for sending/receiving messages. - /// - public Channel Channel { - get { - return this.channel; - } - - set { - ErrorUtilities.VerifyArgumentNotNull(value, "value"); - this.channel = value; - this.AssociationManager.Channel = value; - } - } - - /// - /// Gets the security settings used by this Relying Party. - /// - public RelyingPartySecuritySettings SecuritySettings { - get { - return this.securitySettings; - } - - internal set { - ErrorUtilities.VerifyArgumentNotNull(value, "value"); - this.securitySettings = value; - this.AssociationManager.SecuritySettings = value; - } - } - - /// - /// Gets or sets the optional Provider Endpoint filter to use. - /// - /// - /// Provides a way to optionally filter the providers that may be used in authenticating a user. - /// If provided, the delegate should return true to accept an endpoint, and false to reject it. - /// If null, all identity providers will be accepted. This is the default. - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public EndpointSelector EndpointFilter { get; set; } - - /// - /// Gets or sets the ordering routine that will determine which XRDS - /// Service element to try first - /// - /// Default is . - /// - /// This may never be null. To reset to default behavior this property - /// can be set to the value of . - /// - [EditorBrowsable(EditorBrowsableState.Advanced)] - public Comparison EndpointOrder { - get { - return this.endpointOrder; - } - - set { - ErrorUtilities.VerifyArgumentNotNull(value, "value"); - this.endpointOrder = value; - } - } - - /// - /// Gets a value indicating whether this Relying Party can sign its return_to - /// parameter in outgoing authentication requests. - /// - internal bool CanSignCallbackArguments { - get { return this.Channel.BindingElements.OfType().Any(); } - } - - /// - /// Gets the web request handler to use for discovery and the part of - /// authentication where direct messages are sent to an untrusted remote party. - /// - internal IDirectWebRequestHandler WebRequestHandler { - get { return this.Channel.WebRequestHandler; } - } - - /// - /// Gets the association manager. - /// - internal AssociationManager AssociationManager { get; private set; } - - /// - /// Creates an authentication request to verify that a user controls - /// some given Identifier. - /// - /// - /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. - /// - /// - /// 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/. - /// - /// - /// The URL of the login page, or the page prepared to receive authentication - /// responses from the OpenID Provider. - /// - /// - /// An authentication request object that describes the HTTP response to - /// send to the user agent to initiate the authentication. - /// - /// Thrown if no OpenID endpoint could be found. - public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { - try { - return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First(); - } catch (InvalidOperationException ex) { - throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); - } - } - - /// - /// Creates an authentication request to verify that a user controls - /// some given Identifier. - /// - /// - /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. - /// - /// - /// 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/. - /// - /// - /// An authentication request object that describes the HTTP response to - /// send to the user agent to initiate the authentication. - /// - /// - /// Requires an HttpContext.Current context. - /// - /// Thrown if no OpenID endpoint could be found. - /// Thrown if HttpContext.Current == null. - public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) { - try { - return this.CreateRequests(userSuppliedIdentifier, realm).First(); - } catch (InvalidOperationException ex) { - throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); - } - } - - /// - /// Creates an authentication request to verify that a user controls - /// some given Identifier. - /// - /// - /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. - /// - /// - /// An authentication request object that describes the HTTP response to - /// send to the user agent to initiate the authentication. - /// - /// - /// Requires an HttpContext.Current context. - /// - /// Thrown if no OpenID endpoint could be found. - /// Thrown if HttpContext.Current == null. - public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) { - try { - return this.CreateRequests(userSuppliedIdentifier).First(); - } catch (InvalidOperationException ex) { - throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); - } - } - - /// - /// Gets an authentication response from a Provider. - /// - /// The processed authentication response if there is any; null otherwise. - /// - /// Requires an HttpContext.Current context. - /// - public IAuthenticationResponse GetResponse() { - return this.GetResponse(this.Channel.GetRequestFromContext()); - } - - /// - /// Gets an authentication response from a Provider. - /// - /// The HTTP request that may be carrying an authentication response from the Provider. - /// The processed authentication response if there is any; null otherwise. - public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) { - try { - var message = this.Channel.ReadFromRequest(); - PositiveAssertionResponse positiveAssertion; - NegativeAssertionResponse negativeAssertion; - if ((positiveAssertion = message as PositiveAssertionResponse) != null) { - return new PositiveAuthenticationResponse(positiveAssertion, this); - } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) { - return new NegativeAuthenticationResponse(negativeAssertion); - } else if (message != null) { - Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name); - } - - return null; - } catch (ProtocolException ex) { - return new FailedAuthenticationResponse(ex); - } - } - - /// - /// Determines whether some parameter name belongs to OpenID or this library - /// as a protocol or internal parameter name. - /// - /// Name of the parameter. - /// - /// true if the named parameter is a library- or protocol-specific parameter; otherwise, false. - /// - internal static bool IsOpenIdSupportingParameter(string parameterName) { - Protocol protocol = Protocol.Default; - return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase) - || parameterName.StartsWith("dnoi.", StringComparison.Ordinal); - } - - /// - /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. - /// - /// - /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. - /// - /// - /// 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/. - /// - /// - /// The URL of the login page, or the page prepared to receive authentication - /// responses from the OpenID Provider. - /// - /// - /// An authentication request object that describes the HTTP response to - /// send to the user agent to initiate the authentication. - /// - /// - /// 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 - /// is called. - /// No exception is thrown if no OpenID endpoints were discovered. - /// An empty enumerable is returned instead. - /// - internal IEnumerable CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { - ErrorUtilities.VerifyArgumentNotNull(realm, "realm"); - ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl"); - - return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast(); - } - - /// - /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. - /// - /// - /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. - /// - /// - /// 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/. - /// - /// - /// An authentication request object that describes the HTTP response to - /// send to the user agent to initiate the authentication. - /// - /// - /// 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 - /// is called. - /// No exception is thrown if no OpenID endpoints were discovered. - /// An empty enumerable is returned instead. - /// Requires an HttpContext.Current context. - /// - /// Thrown if HttpContext.Current == null. - internal IEnumerable CreateRequests(Identifier userSuppliedIdentifier, Realm realm) { - ErrorUtilities.VerifyHttpContext(); - - // Build the return_to URL - UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext()); - - // Trim off any parameters with an "openid." prefix, and a few known others - // to avoid carrying state from a prior login attempt. - returnTo.Query = string.Empty; - NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC(); - var returnToParams = new Dictionary(queryParams.Count); - foreach (string key in queryParams) { - if (!IsOpenIdSupportingParameter(key)) { - returnToParams.Add(key, queryParams[key]); - } - } - returnTo.AppendQueryArgs(returnToParams); - - return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri); - } - - /// - /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. - /// - /// - /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. - /// - /// - /// An authentication request object that describes the HTTP response to - /// send to the user agent to initiate the authentication. - /// - /// - /// 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 - /// is called. - /// No exception is thrown if no OpenID endpoints were discovered. - /// An empty enumerable is returned instead. - /// Requires an HttpContext.Current context. - /// - /// Thrown if HttpContext.Current == null. - internal IEnumerable CreateRequests(Identifier userSuppliedIdentifier) { - ErrorUtilities.VerifyHttpContext(); - - // Build the realm URL - UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext()); - realmUrl.Path = HttpContext.Current.Request.ApplicationPath; - realmUrl.Query = null; - realmUrl.Fragment = null; - - // For RP discovery, the realm url MUST NOT redirect. To prevent this for - // virtual directory hosted apps, we need to make sure that the realm path ends - // in a slash (since our calculation above guarantees it doesn't end in a specific - // page like default.aspx). - if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) { - realmUrl.Path += "/"; - } - - return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri)); - } - } -} +//----------------------------------------------------------------------- +// +// Copyright (c) Andrew Arnott. All rights reserved. +// +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Linq; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// + /// A delegate that decides whether a given OpenID Provider endpoint may be + /// considered for authenticating a user. + /// + /// The endpoint for consideration. + /// + /// True if the endpoint should be considered. + /// False to remove it from the pool of acceptable providers. + /// + public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint); + + /// + /// Provides the programmatic facilities to act as an OpenId consumer. + /// + public sealed class OpenIdRelyingParty { + /// + /// The name of the key to use in the HttpApplication cache to store the + /// instance of to use. + /// + private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.ApplicationStore"; + + /// + /// Backing field for the property. + /// + private RelyingPartySecuritySettings securitySettings; + + /// + /// Backing store for the property. + /// + private Comparison endpointOrder = DefaultEndpointOrder; + + /// + /// Backing field for the property. + /// + private Channel channel; + + /// + /// Initializes a new instance of the class. + /// + public OpenIdRelyingParty() + : this(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The application store. If null, the relying party will always operate in "dumb mode". + public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore) + : this(applicationStore, applicationStore, applicationStore) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The association store. If null, the relying party will always operate in "dumb mode". + /// The nonce store to use. If null, the relying party will always operate in "dumb mode". + /// The secret store to use. If null, the relying party will always operate in "dumb mode". + private OpenIdRelyingParty(IAssociationStore associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) { + // If we are a smart-mode RP (supporting associations), then we MUST also be + // capable of storing nonces to prevent replay attacks. + // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays. + ErrorUtilities.VerifyArgument(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore); + + this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings(); + + // Without a nonce store, we must rely on the Provider to protect against + // replay attacks. But only 2.0+ Providers can be expected to provide + // replay protection. + if (nonceStore == null) { + this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20; + } + + this.channel = new OpenIdChannel(associationStore, nonceStore, secretStore, this.SecuritySettings); + this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings); + } + + /// + /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority + /// attribute to determine order. + /// + /// + /// Endpoints lacking any priority value are sorted to the end of the list. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static Comparison DefaultEndpointOrder { + get { return ServiceEndpoint.EndpointOrder; } + } + + /// + /// Gets the standard state storage mechanism that uses ASP.NET's + /// HttpApplication state dictionary to store associations and nonces. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static IRelyingPartyApplicationStore HttpApplicationStore { + get { + HttpContext context = HttpContext.Current; + ErrorUtilities.VerifyOperation(context != null, OpenIdStrings.StoreRequiredWhenNoHttpContextAvailable, typeof(IRelyingPartyApplicationStore).Name); + var store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]; + if (store == null) { + context.Application.Lock(); + try { + if ((store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]) == null) { + context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore(); + } + } finally { + context.Application.UnLock(); + } + } + + return store; + } + } + + /// + /// Gets or sets the channel to use for sending/receiving messages. + /// + public Channel Channel { + get { + return this.channel; + } + + set { + ErrorUtilities.VerifyArgumentNotNull(value, "value"); + this.channel = value; + this.AssociationManager.Channel = value; + } + } + + /// + /// Gets the security settings used by this Relying Party. + /// + public RelyingPartySecuritySettings SecuritySettings { + get { + return this.securitySettings; + } + + internal set { + ErrorUtilities.VerifyArgumentNotNull(value, "value"); + this.securitySettings = value; + this.AssociationManager.SecuritySettings = value; + } + } + + /// + /// Gets or sets the optional Provider Endpoint filter to use. + /// + /// + /// Provides a way to optionally filter the providers that may be used in authenticating a user. + /// If provided, the delegate should return true to accept an endpoint, and false to reject it. + /// If null, all identity providers will be accepted. This is the default. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public EndpointSelector EndpointFilter { get; set; } + + /// + /// Gets or sets the ordering routine that will determine which XRDS + /// Service element to try first + /// + /// Default is . + /// + /// This may never be null. To reset to default behavior this property + /// can be set to the value of . + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Comparison EndpointOrder { + get { + return this.endpointOrder; + } + + set { + ErrorUtilities.VerifyArgumentNotNull(value, "value"); + this.endpointOrder = value; + } + } + + /// + /// Gets a value indicating whether this Relying Party can sign its return_to + /// parameter in outgoing authentication requests. + /// + internal bool CanSignCallbackArguments { + get { return this.Channel.BindingElements.OfType().Any(); } + } + + /// + /// Gets the web request handler to use for discovery and the part of + /// authentication where direct messages are sent to an untrusted remote party. + /// + internal IDirectWebRequestHandler WebRequestHandler { + get { return this.Channel.WebRequestHandler; } + } + + /// + /// Gets the association manager. + /// + internal AssociationManager AssociationManager { get; private set; } + + /// + /// Creates an authentication request to verify that a user controls + /// some given Identifier. + /// + /// + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// + /// + /// 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/. + /// + /// + /// The URL of the login page, or the page prepared to receive authentication + /// responses from the OpenID Provider. + /// + /// + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// + /// Thrown if no OpenID endpoint could be found. + public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + try { + return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First(); + } catch (InvalidOperationException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); + } + } + + /// + /// Creates an authentication request to verify that a user controls + /// some given Identifier. + /// + /// + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// + /// + /// 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/. + /// + /// + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// + /// + /// Requires an HttpContext.Current context. + /// + /// Thrown if no OpenID endpoint could be found. + /// Thrown if HttpContext.Current == null. + public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) { + try { + return this.CreateRequests(userSuppliedIdentifier, realm).First(); + } catch (InvalidOperationException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); + } + } + + /// + /// Creates an authentication request to verify that a user controls + /// some given Identifier. + /// + /// + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// + /// + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// + /// + /// Requires an HttpContext.Current context. + /// + /// Thrown if no OpenID endpoint could be found. + /// Thrown if HttpContext.Current == null. + public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) { + try { + return this.CreateRequests(userSuppliedIdentifier).First(); + } catch (InvalidOperationException ex) { + throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound); + } + } + + /// + /// Gets an authentication response from a Provider. + /// + /// The processed authentication response if there is any; null otherwise. + /// + /// Requires an HttpContext.Current context. + /// + public IAuthenticationResponse GetResponse() { + return this.GetResponse(this.Channel.GetRequestFromContext()); + } + + /// + /// Gets an authentication response from a Provider. + /// + /// The HTTP request that may be carrying an authentication response from the Provider. + /// The processed authentication response if there is any; null otherwise. + public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) { + try { + var message = this.Channel.ReadFromRequest(); + PositiveAssertionResponse positiveAssertion; + NegativeAssertionResponse negativeAssertion; + if ((positiveAssertion = message as PositiveAssertionResponse) != null) { + return new PositiveAuthenticationResponse(positiveAssertion, this); + } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) { + return new NegativeAuthenticationResponse(negativeAssertion); + } else if (message != null) { + Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name); + } + + return null; + } catch (ProtocolException ex) { + return new FailedAuthenticationResponse(ex); + } + } + + /// + /// Determines whether some parameter name belongs to OpenID or this library + /// as a protocol or internal parameter name. + /// + /// Name of the parameter. + /// + /// true if the named parameter is a library- or protocol-specific parameter; otherwise, false. + /// + internal static bool IsOpenIdSupportingParameter(string parameterName) { + Protocol protocol = Protocol.Default; + return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase) + || parameterName.StartsWith("dnoi.", StringComparison.Ordinal); + } + + /// + /// Creates a relying party that does not verify incoming messages against + /// nonce or association stores. + /// + /// The instantiated . + /// + /// Useful for previewing messages while + /// allowing them to be fully processed and verified later. + /// + internal static OpenIdRelyingParty CreateNonVerifying() { + OpenIdRelyingParty rp = new OpenIdRelyingParty(); + rp.Channel = new OpenIdChannel(null, null, null, rp.SecuritySettings); + return rp; + } + + /// + /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. + /// + /// + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// + /// + /// 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/. + /// + /// + /// The URL of the login page, or the page prepared to receive authentication + /// responses from the OpenID Provider. + /// + /// + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// + /// + /// 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 + /// is called. + /// No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead. + /// + internal IEnumerable CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) { + ErrorUtilities.VerifyArgumentNotNull(realm, "realm"); + ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl"); + + return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast(); + } + + /// + /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. + /// + /// + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// + /// + /// 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/. + /// + /// + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// + /// + /// 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 + /// is called. + /// No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead. + /// Requires an HttpContext.Current context. + /// + /// Thrown if HttpContext.Current == null. + internal IEnumerable CreateRequests(Identifier userSuppliedIdentifier, Realm realm) { + ErrorUtilities.VerifyHttpContext(); + + // Build the return_to URL + UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext()); + + // Trim off any parameters with an "openid." prefix, and a few known others + // to avoid carrying state from a prior login attempt. + returnTo.Query = string.Empty; + NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC(); + var returnToParams = new Dictionary(queryParams.Count); + foreach (string key in queryParams) { + if (!IsOpenIdSupportingParameter(key)) { + returnToParams.Add(key, queryParams[key]); + } + } + returnTo.AppendQueryArgs(returnToParams); + + return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri); + } + + /// + /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier. + /// + /// + /// The Identifier supplied by the user. This may be a URL, an XRI or i-name. + /// + /// + /// An authentication request object that describes the HTTP response to + /// send to the user agent to initiate the authentication. + /// + /// + /// 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 + /// is called. + /// No exception is thrown if no OpenID endpoints were discovered. + /// An empty enumerable is returned instead. + /// Requires an HttpContext.Current context. + /// + /// Thrown if HttpContext.Current == null. + internal IEnumerable CreateRequests(Identifier userSuppliedIdentifier) { + ErrorUtilities.VerifyHttpContext(); + + // Build the realm URL + UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext()); + realmUrl.Path = HttpContext.Current.Request.ApplicationPath; + realmUrl.Query = null; + realmUrl.Fragment = null; + + // For RP discovery, the realm url MUST NOT redirect. To prevent this for + // virtual directory hosted apps, we need to make sure that the realm path ends + // in a slash (since our calculation above guarantees it doesn't end in a specific + // page like default.aspx). + if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) { + realmUrl.Path += "/"; + } + + return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri)); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs index 3fb1817..25088ab 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdTextBox.cs @@ -6,7 +6,7 @@ [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdTextBox.EmbeddedLogoResourceName, "image/gif")] -#pragma warning disable 0809 +#pragma warning disable 0809 // marking inherited, unsupported properties as obsolete to discourage their use namespace DotNetOpenAuth.OpenId.RelyingParty { using System; diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index 2deb909..4e76a48 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -12,6 +12,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { using System.Text; using System.Web; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Extensions; using DotNetOpenAuth.OpenId.Messages; /// @@ -64,7 +65,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.VerifyDiscoveryMatchesAssertion(); } - #region IAuthenticationResponse Members + #region IAuthenticationResponse Properties /// /// Gets the Identifier that the end user claims to own. For use with user database storage and lookup. @@ -137,6 +138,17 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { get { return null; } } + #endregion + + /// + /// Gets the positive assertion response message. + /// + internal PositiveAssertionResponse Response { + get { return this.response; } + } + + #region IAuthenticationResponse methods + /// /// Gets a callback argument's value that was previously added using /// . diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.png b/src/DotNetOpenAuth/OpenId/RelyingParty/login_failure.png new file mode 100644 index 0000000000000000000000000000000000000000..8003700a99188597bed6fe71f51340c9e95435de GIT binary patch literal 714 zcwPa@0yX`KP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FZT01FZU(%pXi00007bV*G`2iOS& z1SJp8j-+k?00K%$L_t(I%dL}5NK|1IhM#-AqYg3?3V}bNj5-bILJ~sUm5ZoVh_nd` z+=z>iAfjmRzQxc&q>W&UupcC85kaD0i%QeUpHfWxp`6jVb7$tx{rSGHg*1a8Zt5%# z=Y4tJ^PLa=v9yz#yOU$eT$#t(+CWH2upvS?)x-)-y(9Qx2V&PiC;gZuDz3b-6dowjDUfm>r}fIF>{mV!yha@8D?#2 zLjBblMT2(})OB4ZTw#DHht>s>;Dopc^0rnwg}?X{J!ZM+qM(CADsv zPjj=S2-emH!MZbG1i&Z*DPe!RwCSy7r00O^g7B!3KI=<~QUoC-WK77Uz)V3p3Ew^z z4dmhs(y@`ggC``OP~-}3i6Gbz!AgFoI+q1q2PxoS5AE?Clv6MliS?(5g~9D{97 zN1AD*nL_5R(koO1$@_x5fUiz~<;oUrK76DdJEW%G+kDQJvFR#=b%>aLg@6AVg?m@Y zynRO2gM~tXm-|)p3=W-q%4<^-YGHhY&`yF{FHN~)#}_V>q`l81(%vglxSCX5&OT^ABRyT9o*<2*Z=?k07*qoM6N<$f*_|oqyPW_ literal 0 HcwPel00001 diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).png b/src/DotNetOpenAuth/OpenId/RelyingParty/login_success (lock).png new file mode 100644 index 0000000000000000000000000000000000000000..bc0c0c8b6a69f7794c3259f6aa0624b9fafa65d6 GIT binary patch literal 571 zcwPZJ0>u4^P)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iOS& z1ScoR4Q>_y00Fy6L_t(I%bk+DYZOrwhrfC3WLXO(ugT6rDp>-8wUr1VCPpl5EbIkS zZYPMPpp|9p1wmUuL$xW;|vkU zC#lf5Tfw$%{I-9FV1(A*?)d~!6d@vLt~#21uMbXOnkI1^BO-_hWzWNLoWTjY z-7xKi)|$@YA&s4#!3oCPagUQU002ov JPDHLkV1n;V{N?}v literal 0 HcwPel00001 diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/login_success.png b/src/DotNetOpenAuth/OpenId/RelyingParty/login_success.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae1365d3483b969bb1ee103d037ff505654c13b GIT binary patch literal 464 zcwXxa@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfvP8NP`o&WaYKzYUhPZ!4!i_^(} z{{OdUR&D5PbaL3lc(Z-+QkyJot&>IyzYDfJye?j&{lE6(*#{!;uC+uY2etUcUyRQ8D&9dSG)g>vO_a3FYNKYu#u=#ZMdAanuwg0yIc|E<} zCVF#qxu9%^WcwlI{r`V%dvo%iytzoJ<@1HvZS9WV9%eYRv9+lHadL3(biMY>w6v7S zZx2o!KW@~us5mSoF)J+r2tW>X{;}-A@l{TnjB@_Bq!92zn>he)`&7zVD-P z*R=I#qO5uIt$uv{l>K|A_w)TWze{6%C&XsFnqR+v`MrIW&)1)b|Ec-Ie}f)C-2s;zh6-{_2I*-$9b6IpZ3>2t=ZcZpkS=@y!F6Mxry)N_RK$e>PTsCcu(!a yUsq%=dl)ffd5AGHGyezj-X2*Zc#QFUFN03o^mCR)<`KZ)X7F_Nb6Mw<&;$VD9n7@= literal 0 HcwPel00001 diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif b/src/DotNetOpenAuth/OpenId/RelyingParty/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..9cb298eb4ac260aecbb06f8e701dc7f411286a85 GIT binary patch literal 725 zcwTe&bhEHb6krfwSi}GVhRJ(_s`n};Un}m}J7wvyoqJCK1r`5u`?-b$J39ur8tEA@ zGlGPEvao{G>wpN5b_Qk#j$Hx=Cs>M47szcge6aWCvC?bDdr~9|c^pH!6B*WVpDy|v z_9N#ogXH3X4vzLCNj&W;Z0WWg##K)wxr`rdnuK8}KhRKnj$I0efQE`I<~9i+8R`=l za@qjd(8CEG0*Xf-D@DdgaUE3_mh@$sGF4NB5#3BapqaKDyBv-H%?#JfWl}^kvnMgc zTL5I{rszGQcDD^0)*MadVVe*oSYRWRyG?D9$7;jn4J-%~nScZz)a$E4f3G)P(ZhI2 zaMPNjw~jG}73x0nn0NkKV86l|1BKJ_HQa0LE>Cb+@VJtrb?T!81`EE)4No`yZ1I?B zdUOi1*Aa%=bF4}O8hRo|d)*Y(+-uSoY!fuBCL9aocbud1V3)A9aK;<0!w#HHuIs-u zFjYD`C@>hXO+2Og*<+$na+j($ir1OoUe97Xvu@?rq?XT~QD$>zn)4)Zim)2=Eo)k& zeRH4Y?OiWLSkxS+?3O+8oSC(fiLv2_vC`yIk9b$@X`Hl{1-pq>%-@c=ZjGoG3cjr| zC)(XJS)fNFbJogFYtw&=2;SbMpumwCxM26)6VIhtD_NKjCibG4$N-FRh`)Ck94@dF z59P>ZO1^=Jo^FK@?&rHcSnR*babd#_ftFA%29|jxT}c}P1Vr21)GmilG^_3cMY%Nq DI4kp% literal 0 HcwPel00001 -- 2.11.4.GIT