diff options
13 files changed, 888 insertions, 20 deletions
diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx b/samples/OpenIdRelyingPartyWebForms/login.aspx index 20294e7..42d40e8 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx @@ -23,7 +23,8 @@ <asp:Label ID="setupRequiredLabel" runat="server" EnableViewState="False" Text="You must log into your Provider first to use Immediate mode." Visible="False" /> <p> - <asp:ImageButton runat="server" ImageUrl="~/images/yahoo.png" ID="yahooLoginButton" - OnClick="yahooLoginButton_Click" /> + <rp:OpenIdButton runat="server" ImageUrl="~/images/yahoo.png" + ID="yahooLoginButton" Identifier="https://me.yahoo.com/" + /> </p> </asp:Content> diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx.cs b/samples/OpenIdRelyingPartyWebForms/login.aspx.cs index fb961dd..1de942a 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx.cs +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx.cs @@ -39,16 +39,6 @@ namespace OpenIdRelyingPartyWebForms { this.setupRequiredLabel.Visible = true; } - protected void yahooLoginButton_Click(object sender, ImageClickEventArgs e) { - OpenIdRelyingParty openid = new OpenIdRelyingParty(); - var req = openid.CreateRequest("yahoo.com"); - this.prepareRequest(req); - req.RedirectToProvider(); - - // We don't listen for the response from the provider explicitly - // because the OpenIdLogin control is already doing that for us. - } - private void prepareRequest(IAuthenticationRequest request) { // Collect the PAPE policies requested by the user. List<string> policies = new List<string>(); diff --git a/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs b/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs index 60de3ff..944f5ff 100644 --- a/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs +++ b/samples/OpenIdRelyingPartyWebForms/login.aspx.designer.cs @@ -56,6 +56,6 @@ namespace OpenIdRelyingPartyWebForms { /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// </remarks> - protected global::System.Web.UI.WebControls.ImageButton yahooLoginButton; + protected global::DotNetOpenAuth.OpenId.RelyingParty.OpenIdButton yahooLoginButton; } } diff --git a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs index a7d58c7..37f9c78 100644 --- a/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs +++ b/src/DotNetOpenAuth/ComponentModel/ConverterBase.cs @@ -12,6 +12,9 @@ namespace DotNetOpenAuth.ComponentModel { using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; +using System.Reflection; + using System.Security; + using System.Security.Permissions; /// <summary> /// A design-time helper to allow Intellisense to aid typing @@ -141,6 +144,10 @@ namespace DotNetOpenAuth.ComponentModel { /// The conversion cannot be performed. /// </exception> public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) { + if (destinationType.IsInstanceOfType(value)) { + return value; + } + T typedValue = (T)value; if (destinationType == typeof(string)) { return this.ConvertToString(typedValue); @@ -152,6 +159,20 @@ namespace DotNetOpenAuth.ComponentModel { } /// <summary> + /// Creates an <see cref="InstanceDescriptor"/> instance, protecting against the LinkDemand. + /// </summary> + /// <param name="memberInfo">The member info.</param> + /// <param name="arguments">The arguments.</param> + /// <returns>A <see cref="InstanceDescriptor"/>, or <c>null</c> if sufficient permissions are unavailable.</returns> + protected static InstanceDescriptor CreateInstanceDescriptor(MemberInfo memberInfo, ICollection arguments) { + try { + return CreateInstanceDescriptorPrivate(memberInfo, arguments); + } catch (SecurityException) { + return null; + } + } + + /// <summary> /// Gets the standard values to suggest with Intellisense in the designer. /// </summary> /// <returns>A collection of the standard values.</returns> @@ -185,5 +206,16 @@ namespace DotNetOpenAuth.ComponentModel { /// <returns>The string representation of the object.</returns> [Pure] protected abstract string ConvertToString(T value); + + /// <summary> + /// Creates an <see cref="InstanceDescriptor"/> instance, protecting against the LinkDemand. + /// </summary> + /// <param name="memberInfo">The member info.</param> + /// <param name="arguments">The arguments.</param> + /// <returns>A <see cref="InstanceDescriptor"/>.</returns> + [PermissionSet(SecurityAction.Demand, Name = "FullTrust")] + private static InstanceDescriptor CreateInstanceDescriptorPrivate(MemberInfo memberInfo, ICollection arguments) { + return new InstanceDescriptor(memberInfo, arguments); + } } } diff --git a/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs new file mode 100644 index 0000000..6ba9c4b --- /dev/null +++ b/src/DotNetOpenAuth/ComponentModel/IdentifierConverter.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="IdentifierConverter.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ComponentModel { + using System; + using System.Collections; + using System.ComponentModel.Design.Serialization; + using System.Reflection; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A design-time helper to give an OpenID Identifier property an auto-complete functionality + /// listing the OP Identifiers in the <see cref="WellKnownProviders"/> class. + /// </summary> + public class IdentifierConverter : ConverterBase<Identifier> { + /// <summary> + /// Initializes a new instance of the <see cref="IdentifierConverter"/> class. + /// </summary> + [Obsolete("This class is meant for design-time use within an IDE, and not meant to be used directly by runtime code.")] + public IdentifierConverter() { + } + + /// <summary> + /// Converts a value from its string representation to its strongly-typed object. + /// </summary> + /// <param name="value">The value.</param> + /// <returns>The strongly-typed object.</returns> + protected override Identifier ConvertFrom(string value) { + return value; + } + + /// <summary> + /// Creates the reflection instructions for recreating an instance later. + /// </summary> + /// <param name="value">The value to recreate later.</param> + /// <returns> + /// The description of how to recreate an instance. + /// </returns> + protected override InstanceDescriptor CreateFrom(Identifier value) { + if (value == null) { + return null; + } + + MemberInfo identifierParse = typeof(Identifier).GetMethod("Parse", BindingFlags.Static | BindingFlags.Public); + return CreateInstanceDescriptor(identifierParse, new object[] { value.ToString() }); + } + + /// <summary> + /// Converts the strongly-typed value to a string. + /// </summary> + /// <param name="value">The value to convert.</param> + /// <returns>The string representation of the object.</returns> + protected override string ConvertToString(Identifier value) { + return value; + } + + /// <summary> + /// Gets the standard values to suggest with Intellisense in the designer. + /// </summary> + /// <returns>A collection of the standard values.</returns> + protected override ICollection GetStandardValuesForCache() { + return SuggestedStringsConverter.GetStandardValuesForCacheShared(typeof(WellKnownProviders)); + } + } +} diff --git a/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs b/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs index 3b60bd7..1c8c555 100644 --- a/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs +++ b/src/DotNetOpenAuth/ComponentModel/SuggestedStringsConverter.cs @@ -30,6 +30,19 @@ namespace DotNetOpenAuth.ComponentModel { protected abstract Type WellKnownValuesType { get; } /// <summary> + /// Gets the values of public static fields and properties on a given type. + /// </summary> + /// <param name="type">The type to reflect over.</param> + /// <returns>A collection of values.</returns> + internal static ICollection GetStandardValuesForCacheShared(Type type) { + var fields = from field in type.GetFields(BindingFlags.Static | BindingFlags.Public) + select field.GetValue(null); + var properties = from prop in type.GetProperties(BindingFlags.Static | BindingFlags.Public) + select prop.GetValue(null, null); + return (fields.Concat(properties)).ToArray(); + } + + /// <summary> /// Converts a value from its string representation to its strongly-typed object. /// </summary> /// <param name="value">The value.</param> @@ -68,11 +81,7 @@ namespace DotNetOpenAuth.ComponentModel { /// <returns>A collection of the standard values.</returns> [Pure] protected override ICollection GetStandardValuesForCache() { - var fields = from field in this.WellKnownValuesType.GetFields(BindingFlags.Static | BindingFlags.Public) - select field.GetValue(null); - var properties = from prop in this.WellKnownValuesType.GetProperties(BindingFlags.Static | BindingFlags.Public) - select prop.GetValue(null, null); - return (fields.Concat(properties)).ToArray(); + return GetStandardValuesForCacheShared(this.WellKnownValuesType); } } } diff --git a/src/DotNetOpenAuth/ComponentModel/UriConverter.cs b/src/DotNetOpenAuth/ComponentModel/UriConverter.cs index 4412199..cf8dde3 100644 --- a/src/DotNetOpenAuth/ComponentModel/UriConverter.cs +++ b/src/DotNetOpenAuth/ComponentModel/UriConverter.cs @@ -76,7 +76,7 @@ namespace DotNetOpenAuth.ComponentModel { } MemberInfo uriCtor = typeof(Uri).GetConstructor(new Type[] { typeof(string) }); - return new InstanceDescriptor(uriCtor, new object[] { value.AbsoluteUri }); + return CreateInstanceDescriptor(uriCtor, new object[] { value.AbsoluteUri }); } /// <summary> diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 7a28841..dab793a 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -174,6 +174,7 @@ <Compile Include="ComponentModel\ClaimTypeSuggestions.cs" /> <Compile Include="ComponentModel\ConverterBase.cs" /> <Compile Include="ComponentModel\IssuersSuggestions.cs" /> + <Compile Include="ComponentModel\IdentifierConverter.cs" /> <Compile Include="ComponentModel\SuggestedStringsConverter.cs" /> <Compile Include="ComponentModel\UriConverter.cs" /> <Compile Include="Configuration\AssociationTypeCollection.cs" /> @@ -431,9 +432,11 @@ <Compile Include="OpenId\RelyingParty\AuthenticationRequestMode.cs" /> <Compile Include="OpenId\RelyingParty\NegativeAuthenticationResponse.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdAjaxTextBox.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdButton.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdEventArgs.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdLogin.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdMobileTextBox.cs" /> + <Compile Include="OpenId\RelyingParty\OpenIdRelyingPartyControlBase.cs" /> <Compile Include="OpenId\RelyingParty\OpenIdTextBox.cs" /> <Compile Include="OpenId\RelyingParty\PopupBehavior.cs" /> <Compile Include="OpenId\RelyingParty\PositiveAnonymousResponse.cs" /> @@ -461,6 +464,7 @@ <Compile Include="OpenId\RelyingParty\ServiceEndpoint.cs" /> <Compile Include="OpenId\OpenIdXrdsHelper.cs" /> <Compile Include="OpenId\RelyingParty\StandardRelyingPartyApplicationStore.cs" /> + <Compile Include="OpenId\RelyingParty\WellKnownProviders.cs" /> <Compile Include="OpenId\SecuritySettings.cs" /> <Compile Include="Messaging\UntrustedWebRequestHandler.cs" /> <Compile Include="OpenId\UriIdentifier.cs" /> @@ -558,4 +562,4 @@ </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="..\..\tools\DotNetOpenAuth.Versioning.targets" /> -</Project> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs index f0e8033..76ec9c4 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.Designer.cs @@ -416,6 +416,15 @@ namespace DotNetOpenAuth.OpenId { } /// <summary> + /// Looks up a localized string similar to No identifier has been set.. + /// </summary> + internal static string NoIdentifierSet { + get { + return ResourceManager.GetString("NoIdentifierSet", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to No XRDS document containing OpenID relying party endpoint information could be found at {0}.. /// </summary> internal static string NoRelyingPartyEndpointDiscovered { diff --git a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx index 4f10cf4..fb6fdc7 100644 --- a/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx +++ b/src/DotNetOpenAuth/OpenId/OpenIdStrings.resx @@ -325,4 +325,7 @@ Discovered endpoint info: <data name="UnexpectedEnumPropertyValue" xml:space="preserve"> <value>The property {0} had unexpected value {1}.</value> </data> + <data name="NoIdentifierSet" xml:space="preserve"> + <value>No identifier has been set.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs new file mode 100644 index 0000000..4f930b0 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdButton.cs @@ -0,0 +1,151 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdButton.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Drawing.Design; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Web.UI; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// An ASP.NET control that renders a button that initiates an + /// authentication when clicked. + /// </summary> + public class OpenIdButton : OpenIdRelyingPartyControlBase { + #region Property defaults + + /// <summary> + /// The default value for the <see cref="Identifier"/> property. + /// </summary> + private const string IdentifierDefault = "https://me.yahoo.com/"; + + /// <summary> + /// The default value for the <see cref="Text"/> property. + /// </summary> + private const string TextDefault = "Log in with Yahoo!"; + + #endregion + + #region View state keys + + /// <summary> + /// The key under which the value for the <see cref="Text"/> property will be stored. + /// </summary> + private const string TextViewStateKey = "Text"; + + /// <summary> + /// The key under which the value for the <see cref="ImageUrl"/> property will be stored. + /// </summary> + private const string ImageUrlViewStateKey = "ImageUrl"; + + #endregion + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdButton"/> class. + /// </summary> + public OpenIdButton() { + } + + /// <summary> + /// Gets or sets the text to display for the link. + /// </summary> + [Bindable(true), DefaultValue(TextDefault), Category(AppearanceCategory)] + [Description("The text to display for the link.")] + public string Text { + get { return (string)ViewState[TextViewStateKey] ?? TextDefault; } + set { ViewState[TextViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets the image to display. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] + [Bindable(true), Category(AppearanceCategory)] + [Description("The image to display.")] + [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] + public string ImageUrl { + get { + return (string)ViewState[ImageUrlViewStateKey]; + } + + set { + UriUtil.ValidateResolvableUrl(Page, DesignMode, value); + ViewState[ImageUrlViewStateKey] = value; + } + } + + /// <summary> + /// Raises the <see cref="E:System.Web.UI.Control.PreRender"/> event. + /// </summary> + /// <param name="e">An <see cref="T:System.EventArgs"/> object that contains the event data.</param> + protected override void OnPreRender(EventArgs e) { + base.OnPreRender(e); + + if (!this.DesignMode) { + ErrorUtilities.VerifyOperation(this.Identifier != null, OpenIdStrings.NoIdentifierSet); + } + } + + /// <summary> + /// Sends server control content to a provided <see cref="T:System.Web.UI.HtmlTextWriter"/> object, which writes the content to be rendered on the client. + /// </summary> + /// <param name="writer">The <see cref="T:System.Web.UI.HtmlTextWriter"/> object that receives the server control content.</param> + protected override void Render(HtmlTextWriter writer) { + if (string.IsNullOrEmpty(this.Identifier)) { + writer.WriteEncodedText(string.Format(CultureInfo.CurrentCulture, "[{0}]", OpenIdStrings.NoIdentifierSet)); + } else { + IAuthenticationRequest request = null; + Uri beginUrl = null; + string errorMessage = null; + try { + if (!this.DesignMode) { + request = this.CreateRequest(); + beginUrl = request.RedirectingResponse.GetDirectUriRequest(this.RelyingParty.Channel); + } + } catch (ProtocolException ex) { + Logger.OpenId.Error("OpenIdButton failed to create OpenID request for identifier.", ex); + errorMessage = ex.Message; + } + + if (request == null && !this.DesignMode) { + writer.AddStyleAttribute("text-decoration", "line-through"); + if (!string.IsNullOrEmpty(errorMessage)) { + writer.AddAttribute(HtmlTextWriterAttribute.Title, errorMessage); + } + writer.RenderBeginTag(HtmlTextWriterTag.Span); + } else { + if (beginUrl != null) { + writer.AddAttribute(HtmlTextWriterAttribute.Href, beginUrl.AbsoluteUri); + } + + writer.RenderBeginTag(HtmlTextWriterTag.A); + } + + if (!string.IsNullOrEmpty(this.ImageUrl)) { + writer.AddAttribute(HtmlTextWriterAttribute.Src, this.ResolveClientUrl(this.ImageUrl)); + writer.AddAttribute(HtmlTextWriterAttribute.Border, "0"); + string tooltip = errorMessage ?? this.Text; + if (!string.IsNullOrEmpty(tooltip)) { + writer.AddAttribute(HtmlTextWriterAttribute.Alt, tooltip); + writer.AddAttribute(HtmlTextWriterAttribute.Title, tooltip); + } + writer.RenderBeginTag(HtmlTextWriterTag.Img); + writer.RenderEndTag(); + } else if (!string.IsNullOrEmpty(this.Text)) { + writer.WriteEncodedText(this.Text); + } + + writer.RenderEndTag(); + } + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs new file mode 100644 index 0000000..a35d532 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingPartyControlBase.cs @@ -0,0 +1,567 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdRelyingPartyControlBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Drawing.Design; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Web.Security; + using System.Web.UI; + using DotNetOpenAuth.ComponentModel; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Extensions.UI; + + /// <summary> + /// A common base class for OpenID Relying Party controls. + /// </summary> + public abstract class OpenIdRelyingPartyControlBase : Control { + #region Property category constants + + /// <summary> + /// The "Appearance" category for properties. + /// </summary> + protected const string AppearanceCategory = "Appearance"; + + /// <summary> + /// The "Behavior" category for properties. + /// </summary> + protected const string BehaviorCategory = "Behavior"; + + #endregion + + #region Property default values + + /// <summary> + /// The default value for the <see cref="Stateless"/> property. + /// </summary> + private const bool StatelessDefault = false; + + /// <summary> + /// Default value of <see cref="UsePersistentCookie"/>. + /// </summary> + private const bool UsePersistentCookieDefault = false; + + /// <summary> + /// The default value for the <see cref="ReturnToUrl"/> property. + /// </summary> + private const string ReturnToUrlDefault = ""; + + /// <summary> + /// The default value for the <see cref="RealmUrl"/> property. + /// </summary> + private const string RealmUrlDefault = "~/"; + + /// <summary> + /// The default value for the <see cref="Popup"/> property. + /// </summary> + private const PopupBehavior PopupDefault = PopupBehavior.Never; + + /// <summary> + /// The default value for the <see cref="RequireSsl"/> property. + /// </summary> + private const bool RequireSslDefault = false; + + #endregion + + #region Property view state keys + + /// <summary> + /// The viewstate key to use for the <see cref="Stateless"/> property. + /// </summary> + private const string StatelessViewStateKey = "Stateless"; + + /// <summary> + /// The viewstate key to use for the <see cref="UsePersistentCookie"/> property. + /// </summary> + private const string UsePersistentCookieViewStateKey = "UsePersistentCookie"; + + /// <summary> + /// The viewstate key to use for the <see cref="RealmUrl"/> property. + /// </summary> + private const string RealmUrlViewStateKey = "RealmUrl"; + + /// <summary> + /// The viewstate key to use for the <see cref="ReturnToUrl"/> property. + /// </summary> + private const string ReturnToUrlViewStateKey = "ReturnToUrl"; + + /// <summary> + /// The key under which the value for the <see cref="Identifier"/> property will be stored. + /// </summary> + private const string IdentifierViewStateKey = "Identifier"; + + /// <summary> + /// The viewstate key to use for the <see cref="Popup"/> property. + /// </summary> + private const string PopupViewStateKey = "Popup"; + + /// <summary> + /// The viewstate key to use for the <see cref="RequireSsl"/> property. + /// </summary> + private const string RequireSslViewStateKey = "RequireSsl"; + + #endregion + + #region Callback parameter names + + /// <summary> + /// The callback parameter for use with persisting the <see cref="UsePersistentCookie"/> property. + /// </summary> + private const string UsePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie"; + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in a popup window. + /// </summary> + private const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup"; + + /// <summary> + /// The callback parameter to use for recognizing when the callback is in the parent window. + /// </summary> + private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent"; + + #endregion + + /// <summary> + /// Backing field for the <see cref="RelyingParty"/> property. + /// </summary> + private OpenIdRelyingParty relyingParty; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdRelyingPartyControlBase"/> class. + /// </summary> + protected OpenIdRelyingPartyControlBase() { + } + + #region Events + + /// <summary> + /// Fired after the user clicks the log in button, but before the authentication + /// process begins. Offers a chance for the web application to disallow based on + /// OpenID URL before redirecting the user to the OpenID Provider. + /// </summary> + [Description("Fired after the user clicks the log in button, but before the authentication process begins. Offers a chance for the web application to disallow based on OpenID URL before redirecting the user to the OpenID Provider.")] + public event EventHandler<OpenIdEventArgs> LoggingIn; + + /// <summary> + /// Fired upon completion of a successful login. + /// </summary> + [Description("Fired upon completion of a successful login.")] + public event EventHandler<OpenIdEventArgs> LoggedIn; + + /// <summary> + /// Fired when a login attempt fails. + /// </summary> + [Description("Fired when a login attempt fails.")] + public event EventHandler<OpenIdEventArgs> Failed; + + /// <summary> + /// Fired when an authentication attempt is canceled at the OpenID Provider. + /// </summary> + [Description("Fired when an authentication attempt is canceled at the OpenID Provider.")] + public event EventHandler<OpenIdEventArgs> Canceled; + + #endregion + + /// <summary> + /// Gets or sets the <see cref="OpenIdRelyingParty"/> instance to use. + /// </summary> + /// <value>The default value is an <see cref="OpenIdRelyingParty"/> instance initialized according to the web.config file.</value> + /// <remarks> + /// A performance optimization would be to store off the + /// instance as a static member in your web site and set it + /// to this property in your <see cref="Control.Load">Page.Load</see> + /// event since instantiating these instances can be expensive on + /// heavily trafficked web pages. + /// </remarks> + [Browsable(false)] + public OpenIdRelyingParty RelyingParty { + get { + if (this.relyingParty == null) { + this.relyingParty = this.CreateRelyingParty(); + } + return this.relyingParty; + } + + set { + this.relyingParty = value; + } + } + + /// <summary> + /// Gets or sets a value indicating whether stateless mode is used. + /// </summary> + [Bindable(true), DefaultValue(StatelessDefault), Category(BehaviorCategory)] + [Description("Controls whether stateless mode is used.")] + public bool Stateless { + get { return (bool)(ViewState[StatelessViewStateKey] ?? StatelessDefault); } + set { ViewState[StatelessViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets the OpenID <see cref="Realm"/> of the relying party web site. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] + [Bindable(true), DefaultValue(RealmUrlDefault), Category(BehaviorCategory)] + [Description("The OpenID Realm of the relying party web site.")] + [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] + public string RealmUrl { + get { + return (string)(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(this.Page, value, this.RelyingParty.Channel.GetRequestFromContext())); // 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("*.", string.Empty)); // make sure it's fully-qualified, but ignore wildcards + } else if (value.StartsWith("~/", StringComparison.Ordinal)) { + // this is valid too + } else { + throw new UriFormatException(); + } + } + ViewState[RealmUrlViewStateKey] = value; + } + } + + /// <summary> + /// Gets or sets the OpenID ReturnTo of the relying party web site. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Bindable property must be simple type")] + [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] + [Bindable(true), DefaultValue(ReturnToUrlDefault), Category(BehaviorCategory)] + [Description("The OpenID ReturnTo of the relying party web site.")] + [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] + public string ReturnToUrl { + get { + return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault); + } + + set { + if (this.Page != null && !this.DesignMode) { + // Validate new value by trying to construct a Uri based on it. + new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.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; + } + } + + /// <summary> + /// Gets or sets a value indicating whether to send a persistent cookie upon successful + /// login so the user does not have to log in upon returning to this site. + /// </summary> + [Bindable(true), DefaultValue(UsePersistentCookieDefault), Category(BehaviorCategory)] + [Description("Whether to send a persistent cookie upon successful " + + "login so the user does not have to log in upon returning to this site.")] + public virtual bool UsePersistentCookie { + get { return (bool)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); } + set { this.ViewState[UsePersistentCookieViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets a value indicating when to use a popup window to complete the login experience. + /// </summary> + /// <value>The default value is <see cref="PopupBehavior.Never"/>.</value> + [Bindable(true), DefaultValue(PopupDefault), Category(BehaviorCategory)] + [Description("When to use a popup window to complete the login experience.")] + public PopupBehavior Popup { + get { return (PopupBehavior)(ViewState[PopupViewStateKey] ?? PopupDefault); } + set { ViewState[PopupViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets a value indicating whether to enforce on high security mode, + /// which requires the full authentication pipeline to be protected by SSL. + /// </summary> + [Bindable(true), DefaultValue(RequireSslDefault), Category(BehaviorCategory)] + [Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")] + public bool RequireSsl { + get { return (bool)(ViewState[RequireSslViewStateKey] ?? RequireSslDefault); } + set { ViewState[RequireSslViewStateKey] = value; } + } + + /// <summary> + /// Gets or sets the URL to your privacy policy page that describes how + /// claims will be used and/or shared. + /// </summary> + [Bindable(true), Category(BehaviorCategory)] + [Description("The OpenID Identifier that this button will use to initiate login.")] + [TypeConverter(typeof(IdentifierConverter))] + public Identifier Identifier { + get { return (Identifier)ViewState[IdentifierViewStateKey]; } + set { ViewState[IdentifierViewStateKey] = value; } + } + + /// <summary> + /// Immediately redirects to the OpenID Provider to verify the Identifier + /// provided in the text box. + /// </summary> + public void LogOn() { + IAuthenticationRequest request = this.CreateRequest(); + if (this.IsPopupAppropriate(request)) { + this.ScriptPopupWindow(request); + } else { + request.RedirectToProvider(); + } + } + + /// <summary> + /// Constructs the authentication request and returns it. + /// </summary> + /// <returns>The instantiated authentication request, or <c>null</c> if a failure occurred.</returns> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] + protected virtual IAuthenticationRequest CreateRequest() { + Contract.Requires(this.Identifier != null, OpenIdStrings.OpenIdTextBoxEmpty); + ErrorUtilities.VerifyOperation(!string.IsNullOrEmpty(this.Identifier), OpenIdStrings.OpenIdTextBoxEmpty); + IAuthenticationRequest request; + + try { + // Approximate the returnTo (either based on the customize property or the page URL) + // so we can use it to help with Realm resolution. + var requestContext = this.RelyingParty.Channel.GetRequestFromContext(); + Uri returnToApproximation = this.ReturnToUrl != null ? new Uri(requestContext.UrlBeforeRewriting, this.ReturnToUrl) : this.Page.Request.Url; + + // 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, this.RelyingParty.Channel.GetRequestFromContext()); + realm.Scheme = returnToApproximation.Scheme; + realm.Port = returnToApproximation.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)) { + request = this.RelyingParty.CreateRequest(this.Identifier, typedRealm); + } else { + // Since the user actually gave us a return_to value, + // the "approximation" is exactly what we want. + request = this.RelyingParty.CreateRequest(this.Identifier, typedRealm, returnToApproximation); + } + + if (this.IsPopupAppropriate(request)) { + // Inform the OP that it will appear in a popup window. + request.AddExtension(new UIRequest()); + } + + // Add state that needs to survive across the redirect. + if (!this.Stateless) { + request.AddCallbackArguments(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); + } + + this.OnLoggingIn(request); + } catch (ProtocolException ex) { + this.OnFailed(new FailedAuthenticationResponse(ex)); + return null; + } + + return request; + } + + /// <summary> + /// Raises the <see cref="E:Load"/> event. + /// </summary> + /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param> + protected override void OnLoad(EventArgs e) { + base.OnLoad(e); + + if (Page.IsPostBack) { + // OpenID responses NEVER come in the form of a postback. + return; + } + + var response = this.RelyingParty.GetResponse(); + if (response != null) { + switch (response.Status) { + case AuthenticationStatus.Authenticated: + this.OnLoggedIn(response); + break; + case AuthenticationStatus.Canceled: + this.OnCanceled(response); + break; + case AuthenticationStatus.Failed: + this.OnFailed(response); + break; + case AuthenticationStatus.SetupRequired: + case AuthenticationStatus.ExtensionsOnly: + default: + // The NotApplicable (extension-only assertion) is NOT one that we support + // in this control because that scenario is primarily interesting to RPs + // that are asking a specific OP, and it is not user-initiated as this textbox + // is designed for. + throw new InvalidOperationException(MessagingStrings.UnexpectedMessageReceivedOfMany); + } + } + } + + /// <summary> + /// Fires the <see cref="LoggedIn"/> event. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void OnLoggedIn(IAuthenticationResponse response) { + Contract.Requires(response != null); + Contract.Requires(response.Status == AuthenticationStatus.Authenticated); + ErrorUtilities.VerifyArgumentNotNull(response, "response"); + ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Authenticated, "Firing OnLoggedIn event without an authenticated response."); + + var loggedIn = this.LoggedIn; + OpenIdEventArgs args = new OpenIdEventArgs(response); + if (loggedIn != null) { + loggedIn(this, args); + } + + if (!args.Cancel) { + FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie); + } + } + + /// <summary> + /// Fires the <see cref="LoggingIn"/> event. + /// </summary> + /// <param name="request">The request.</param> + /// <returns> + /// Returns whether the login should proceed. False if some event handler canceled the request. + /// </returns> + protected virtual bool OnLoggingIn(IAuthenticationRequest request) { + Contract.Requires(request != null); + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + EventHandler<OpenIdEventArgs> loggingIn = this.LoggingIn; + + OpenIdEventArgs args = new OpenIdEventArgs(request); + if (loggingIn != null) { + loggingIn(this, args); + } + + return !args.Cancel; + } + + /// <summary> + /// Fires the <see cref="Canceled"/> event. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void OnCanceled(IAuthenticationResponse response) { + Contract.Requires(response != null); + Contract.Requires(response.Status == AuthenticationStatus.Canceled); + ErrorUtilities.VerifyArgumentNotNull(response, "response"); + ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Canceled, "Firing Canceled event for the wrong response type."); + + var canceled = this.Canceled; + if (canceled != null) { + canceled(this, new OpenIdEventArgs(response)); + } + } + + /// <summary> + /// Fires the <see cref="Failed"/> event. + /// </summary> + /// <param name="response">The response.</param> + protected virtual void OnFailed(IAuthenticationResponse response) { + Contract.Requires(response != null); + Contract.Requires(response.Status == AuthenticationStatus.Failed); + ErrorUtilities.VerifyArgumentNotNull(response, "response"); + ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.Failed, "Firing Failed event for the wrong response type."); + + var failed = this.Failed; + if (failed != null) { + failed(this, new OpenIdEventArgs(response)); + } + } + + /// <summary> + /// Creates the relying party instance used to generate authentication requests. + /// </summary> + /// <returns>The instantiated relying party.</returns> + protected virtual OpenIdRelyingParty CreateRelyingParty() { + IRelyingPartyApplicationStore store = this.Stateless ? null : DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore); + var rp = new OpenIdRelyingParty(store); + + // Only set RequireSsl to true, as we don't want to override + // a .config setting of true with false. + if (this.RequireSsl) { + rp.SecuritySettings.RequireSsl = true; + } + + return rp; + } + + /// <summary> + /// Detects whether a popup window should be used to show the Provider's UI + /// and applies the UI extension to the request when appropriate. + /// </summary> + /// <param name="request">The request.</param> + /// <returns> + /// <c>true</c> if a popup should be used; <c>false</c> otherwise. + /// </returns> + private bool IsPopupAppropriate(IAuthenticationRequest request) { + Contract.Requires(request != null); + ErrorUtilities.VerifyArgumentNotNull(request, "request"); + + return this.Popup == PopupBehavior.Always || request.Provider.IsExtensionSupported<UIRequest>(); + } + + /// <summary> + /// Wires the return page to immediately display a popup window with the Provider in it. + /// </summary> + /// <param name="request">The request.</param> + private void ScriptPopupWindow(IAuthenticationRequest request) { + Contract.Requires(request != null); + Contract.Requires(this.RelyingParty != null); + + request.AddCallbackArguments(UIPopupCallbackKey, "1"); + + StringBuilder startupScript = new StringBuilder(); + + // Add a callback function that the popup window can call on this, the + // parent window, to pass back the authentication result. + startupScript.AppendLine("window.dnoa_internal = new Object();"); + startupScript.AppendLine("window.dnoa_internal.processAuthorizationResult = function(uri) { window.location = uri; };"); + + // Open the popup window. + startupScript.AppendFormat( + @"var openidPopup = {0}", + UIUtilities.GetWindowPopupScript(this.RelyingParty, request, "openidPopup")); + + this.Page.ClientScript.RegisterStartupScript(this.GetType(), "loginPopup", startupScript.ToString(), true); + } + + /// <summary> + /// Wires the popup window to close itself and pass the authentication result to the parent window. + /// </summary> + private void ScriptClosingPopup() { + StringBuilder startupScript = new StringBuilder(); + startupScript.AppendLine("window.opener.dnoa_internal.processAuthorizationResult(document.URL + '&" + UIPopupCallbackParentKey + "=1');"); + startupScript.AppendLine("window.close();"); + + this.Page.ClientScript.RegisterStartupScript(this.GetType(), "loginPopupClose", startupScript.ToString(), true); + } + } +} diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs new file mode 100644 index 0000000..bd45842 --- /dev/null +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/WellKnownProviders.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="WellKnownProviders.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + /// <summary> + /// Common OpenID Provider Identifiers. + /// </summary> + public sealed class WellKnownProviders { + /// <summary> + /// The Yahoo OP Identifier. + /// </summary> + public static readonly Identifier Yahoo = "https://me.yahoo.com/"; + + /// <summary> + /// The Google OP Identifier. + /// </summary> + public static readonly Identifier Google = "https://www.google.com/accounts/o8/id"; + + /// <summary> + /// The MyOpenID OP Identifier. + /// </summary> + public static readonly Identifier MyOpenId = "https://www.myopenid.com/"; + + /// <summary> + /// Prevents a default instance of the <see cref="WellKnownProviders"/> class from being created. + /// </summary> + private WellKnownProviders() { + } + } +} |