//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // //----------------------------------------------------------------------- [assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdMobileTextBox.EmbeddedLogoResourceName, "image/gif")] namespace DotNetOpenAuth.OpenId.RelyingParty { using System; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.Text.RegularExpressions; using System.Web.Security; using System.Web.UI; using System.Web.UI.MobileControls; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration; /// /// An ASP.NET control for mobile devices that provides a minimal text box that is OpenID-aware. /// [DefaultProperty("Text"), ValidationProperty("Text")] [ToolboxData("<{0}:OpenIdMobileTextBox runat=\"server\" />")] public class OpenIdMobileTextBox : TextBox { /// /// The name of the manifest stream containing the /// OpenID logo that is placed inside the text box. /// internal const string EmbeddedLogoResourceName = OpenIdTextBox.EmbeddedLogoResourceName; /// /// Default value of . /// protected const bool UsePersistentCookieDefault = false; #region Property category constants /// /// The "Appearance" category for properties. /// private const string AppearanceCategory = "Appearance"; /// /// The "Simple Registration" category for properties. /// private const string ProfileCategory = "Simple Registration"; /// /// The "Behavior" category for properties. /// private const string BehaviorCategory = "Behavior"; #endregion #region Property viewstate keys /// /// The viewstate key to use for the property. /// private const string RequestEmailViewStateKey = "RequestEmail"; /// /// The viewstate key to use for the property. /// private const string RequestNicknameViewStateKey = "RequestNickname"; /// /// The viewstate key to use for the property. /// private const string RequestPostalCodeViewStateKey = "RequestPostalCode"; /// /// The viewstate key to use for the property. /// private const string RequestCountryViewStateKey = "RequestCountry"; /// /// The viewstate key to use for the property. /// private const string RequireSslViewStateKey = "RequireSsl"; /// /// The viewstate key to use for the property. /// private const string RequestLanguageViewStateKey = "RequestLanguage"; /// /// The viewstate key to use for the property. /// private const string RequestTimeZoneViewStateKey = "RequestTimeZone"; /// /// The viewstate key to use for the property. /// private const string EnableRequestProfileViewStateKey = "EnableRequestProfile"; /// /// The viewstate key to use for the property. /// private const string PolicyUrlViewStateKey = "PolicyUrl"; /// /// The viewstate key to use for the property. /// private const string RequestFullNameViewStateKey = "RequestFullName"; /// /// The viewstate key to use for the property. /// private const string UsePersistentCookieViewStateKey = "UsePersistentCookie"; /// /// The viewstate key to use for the property. /// private const string RequestGenderViewStateKey = "RequestGender"; /// /// The viewstate key to use for the property. /// private const string ReturnToUrlViewStateKey = "ReturnToUrl"; /// /// The viewstate key to use for the property. /// private const string StatelessViewStateKey = "Stateless"; /// /// The viewstate key to use for the property. /// private const string ImmediateModeViewStateKey = "ImmediateMode"; /// /// The viewstate key to use for the property. /// private const string RequestBirthDateViewStateKey = "RequestBirthDate"; /// /// The viewstate key to use for the property. /// private const string RealmUrlViewStateKey = "RealmUrl"; #endregion #region Property defaults /// /// The default value for the property. /// private const bool EnableRequestProfileDefault = true; /// /// The default value for the property. /// private const bool RequireSslDefault = false; /// /// The default value for the property. /// private const bool ImmediateModeDefault = false; /// /// The default value for the property. /// private const bool StatelessDefault = false; /// /// The default value for the property. /// private const string PolicyUrlDefault = ""; /// /// 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 DemandLevel RequestEmailDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestPostalCodeDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestCountryDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestLanguageDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestTimeZoneDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestNicknameDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestFullNameDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestBirthDateDefault = DemandLevel.NoRequest; /// /// The default value for the property. /// private const DemandLevel RequestGenderDefault = DemandLevel.NoRequest; #endregion /// /// The callback parameter for use with persisting the property. /// private const string UsePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie"; /// /// Backing field for the property. /// private OpenIdRelyingParty relyingParty; /// /// Initializes a new instance of the class. /// public OpenIdMobileTextBox() { Reporting.RecordFeatureUse(this); } #region Events /// /// Fired upon completion of a successful login. /// [Description("Fired upon completion of a successful login.")] public event EventHandler LoggedIn; /// /// Fired when a login attempt fails. /// [Description("Fired when a login attempt fails.")] public event EventHandler Failed; /// /// Fired when an authentication attempt is canceled at the OpenID Provider. /// [Description("Fired when an authentication attempt is canceled at the OpenID Provider.")] public event EventHandler Canceled; /// /// Fired when an Immediate authentication attempt fails, and the Provider suggests using non-Immediate mode. /// [Description("Fired when an Immediate authentication attempt fails, and the Provider suggests using non-Immediate mode.")] public event EventHandler SetupRequired; #endregion #region Properties /// /// Gets or sets the OpenID of the relying party web site. /// [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using Realm.ctor for validation.")] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId", 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.")] 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; } } /// /// Gets or sets the OpenID ReturnTo of the relying party web site. /// [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] [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.")] public string ReturnToUrl { get { return (string)(ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault); } set { if (Page != null && !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(); } } ViewState[ReturnToUrlViewStateKey] = value; } } /// /// Gets or sets a value indicating whether to use immediate mode in the /// OpenID protocol. /// /// /// True if a Provider should reply immediately to the authentication request /// without interacting with the user. False if the Provider can take time /// to authenticate the user in order to complete an authentication attempt. /// /// /// Setting this to true is sometimes useful in AJAX scenarios. Setting this to /// true can cause failed authentications when the user truly controls an /// Identifier, but must complete an authentication step with the Provider before /// the Provider will approve the login from this relying party. /// [Bindable(true), DefaultValue(ImmediateModeDefault), Category(BehaviorCategory)] [Description("Whether the Provider should respond immediately to an authentication attempt without interacting with the user.")] public bool ImmediateMode { get { return (bool)(ViewState[ImmediateModeViewStateKey] ?? ImmediateModeDefault); } set { ViewState[ImmediateModeViewStateKey] = value; } } /// /// Gets or sets a value indicating whether stateless mode is used. /// [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; } } /// /// 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. /// [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; } } /// /// Gets or sets your level of interest in receiving the user's nickname from the Provider. /// [Bindable(true), DefaultValue(RequestNicknameDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's nickname from the Provider.")] public DemandLevel RequestNickname { get { return (DemandLevel)(ViewState[RequestNicknameViewStateKey] ?? RequestNicknameDefault); } set { ViewState[RequestNicknameViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's email address from the Provider. /// [Bindable(true), DefaultValue(RequestEmailDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's email address from the Provider.")] public DemandLevel RequestEmail { get { return (DemandLevel)(ViewState[RequestEmailViewStateKey] ?? RequestEmailDefault); } set { ViewState[RequestEmailViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's full name from the Provider. /// [Bindable(true), DefaultValue(RequestFullNameDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's full name from the Provider")] public DemandLevel RequestFullName { get { return (DemandLevel)(ViewState[RequestFullNameViewStateKey] ?? RequestFullNameDefault); } set { ViewState[RequestFullNameViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's birthdate from the Provider. /// [Bindable(true), DefaultValue(RequestBirthDateDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's birthdate from the Provider.")] public DemandLevel RequestBirthDate { get { return (DemandLevel)(ViewState[RequestBirthDateViewStateKey] ?? RequestBirthDateDefault); } set { ViewState[RequestBirthDateViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's gender from the Provider. /// [Bindable(true), DefaultValue(RequestGenderDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's gender from the Provider.")] public DemandLevel RequestGender { get { return (DemandLevel)(ViewState[RequestGenderViewStateKey] ?? RequestGenderDefault); } set { ViewState[RequestGenderViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's postal code from the Provider. /// [Bindable(true), DefaultValue(RequestPostalCodeDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's postal code from the Provider.")] public DemandLevel RequestPostalCode { get { return (DemandLevel)(ViewState[RequestPostalCodeViewStateKey] ?? RequestPostalCodeDefault); } set { ViewState[RequestPostalCodeViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's country from the Provider. /// [Bindable(true)] [Category(ProfileCategory)] [DefaultValue(RequestCountryDefault)] [Description("Your level of interest in receiving the user's country from the Provider.")] public DemandLevel RequestCountry { get { return (DemandLevel)(ViewState[RequestCountryViewStateKey] ?? RequestCountryDefault); } set { ViewState[RequestCountryViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's preferred language from the Provider. /// [Bindable(true), DefaultValue(RequestLanguageDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's preferred language from the Provider.")] public DemandLevel RequestLanguage { get { return (DemandLevel)(ViewState[RequestLanguageViewStateKey] ?? RequestLanguageDefault); } set { ViewState[RequestLanguageViewStateKey] = value; } } /// /// Gets or sets your level of interest in receiving the user's time zone from the Provider. /// [Bindable(true), DefaultValue(RequestTimeZoneDefault), Category(ProfileCategory)] [Description("Your level of interest in receiving the user's time zone from the Provider.")] public DemandLevel RequestTimeZone { get { return (DemandLevel)(ViewState[RequestTimeZoneViewStateKey] ?? RequestTimeZoneDefault); } set { ViewState[RequestTimeZoneViewStateKey] = value; } } /// /// Gets or sets the URL to your privacy policy page that describes how /// claims will be used and/or shared. /// [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")] [Bindable(true), DefaultValue(PolicyUrlDefault), Category(ProfileCategory)] [Description("The URL to your privacy policy page that describes how claims will be used and/or shared.")] public string PolicyUrl { get { return (string)ViewState[PolicyUrlViewStateKey] ?? PolicyUrlDefault; } set { UriUtil.ValidateResolvableUrl(Page, DesignMode, value); ViewState[PolicyUrlViewStateKey] = value; } } /// /// Gets or sets a value indicating whether to use OpenID extensions /// to retrieve profile data of the authenticating user. /// [Bindable(true), DefaultValue(EnableRequestProfileDefault), Category(ProfileCategory)] [Description("Turns the entire Simple Registration extension on or off.")] public bool EnableRequestProfile { get { return (bool)(ViewState[EnableRequestProfileViewStateKey] ?? EnableRequestProfileDefault); } set { ViewState[EnableRequestProfileViewStateKey] = value; } } /// /// Gets or sets a value indicating whether to enforce on high security mode, /// which requires the full authentication pipeline to be protected by SSL. /// [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; } } /// /// Gets or sets the type of the custom application store to use, or null to use the default. /// /// /// If set, this property must be set in each Page Load event /// as it is not persisted across postbacks. /// public IOpenIdApplicationStore CustomApplicationStore { get; set; } #endregion /// /// Gets or sets the instance to use. /// /// The default value is an instance initialized according to the web.config file. /// /// 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 Page.Load /// event since instantiating these instances can be expensive on /// heavily trafficked web pages. /// public OpenIdRelyingParty RelyingParty { get { if (this.relyingParty == null) { this.relyingParty = this.CreateRelyingParty(); } return this.relyingParty; } set { this.relyingParty = value; } } /// /// Gets or sets the OpenID authentication request that is about to be sent. /// protected IAuthenticationRequest Request { get; set; } /// /// Immediately redirects to the OpenID Provider to verify the Identifier /// provided in the text box. /// public void LogOn() { if (this.Request == null) { this.CreateRequest(); // sets this.Request } if (this.Request != null) { this.Request.RedirectToProvider(); } } /// /// Constructs the authentication request and returns it. /// /// The instantiated authentication request. /// /// This method need not be called before calling the method, /// but is offered in the event that adding extensions to the request is desired. /// The Simple Registration extension arguments are added to the request /// before returning if is set to true. /// [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")] public IAuthenticationRequest CreateRequest() { Contract.Requires(this.Request == null, OpenIdStrings.CreateRequestAlreadyCalled); Contract.Requires(!string.IsNullOrEmpty(this.Text), OpenIdStrings.OpenIdTextBoxEmpty); try { // 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 = 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. Identifier userSuppliedIdentifier; if (Identifier.TryParse(this.Text, out userSuppliedIdentifier)) { Realm typedRealm = new Realm(realm); if (string.IsNullOrEmpty(this.ReturnToUrl)) { this.Request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, typedRealm); } else { Uri returnTo = new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.ReturnToUrl); this.Request = this.RelyingParty.CreateRequest(userSuppliedIdentifier, typedRealm, returnTo); } this.Request.Mode = this.ImmediateMode ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup; if (this.EnableRequestProfile) { this.AddProfileArgs(this.Request); } // Add state that needs to survive across the redirect. this.Request.SetUntrustedCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString(CultureInfo.InvariantCulture)); } else { Logger.OpenId.WarnFormat("An invalid identifier was entered ({0}), but not caught by any validation routine.", this.Text); this.Request = null; } } catch (ProtocolException ex) { this.OnFailed(new FailedAuthenticationResponse(ex)); } return this.Request; } /// /// Checks for incoming OpenID authentication responses and fires appropriate events. /// /// The object that contains the event data. protected override void OnLoad(EventArgs e) { base.OnLoad(e); if (Page.IsPostBack) { return; } var response = this.RelyingParty.GetResponse(); if (response != null) { string persistentString = response.GetUntrustedCallbackArgument(UsePersistentCookieCallbackKey); bool persistentBool; if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) { this.UsePersistentCookie = persistentBool; } switch (response.Status) { case AuthenticationStatus.Canceled: this.OnCanceled(response); break; case AuthenticationStatus.Authenticated: this.OnLoggedIn(response); break; case AuthenticationStatus.SetupRequired: this.OnSetupRequired(response); break; case AuthenticationStatus.Failed: this.OnFailed(response); break; default: throw new InvalidOperationException("Unexpected response status code."); } } } #region Events /// /// Fires the event. /// /// The response. protected virtual void OnLoggedIn(IAuthenticationResponse response) { Contract.Requires(response != null); 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); } } /// /// Fires the event. /// /// The response. protected virtual void OnFailed(IAuthenticationResponse response) { Contract.Requires(response != null); 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)); } } /// /// Fires the event. /// /// The response. protected virtual void OnCanceled(IAuthenticationResponse response) { Contract.Requires(response != null); 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)); } } /// /// Fires the event. /// /// The response. protected virtual void OnSetupRequired(IAuthenticationResponse response) { Contract.Requires(response != null); ErrorUtilities.VerifyInternal(response.Status == AuthenticationStatus.SetupRequired, "Firing SetupRequired event for the wrong response type."); // Why are we firing Failed when we're OnSetupRequired? Backward compatibility. var setupRequired = this.SetupRequired; if (setupRequired != null) { setupRequired(this, new OpenIdEventArgs(response)); } } #endregion /// /// Adds extensions to a given authentication request to ask the Provider /// for user profile data. /// /// The authentication request to add the extensions to. private void AddProfileArgs(IAuthenticationRequest request) { Contract.Requires(request != null); request.AddExtension(new ClaimsRequest() { Nickname = this.RequestNickname, Email = this.RequestEmail, FullName = this.RequestFullName, BirthDate = this.RequestBirthDate, Gender = this.RequestGender, PostalCode = this.RequestPostalCode, Country = this.RequestCountry, Language = this.RequestLanguage, TimeZone = this.RequestTimeZone, PolicyUrl = string.IsNullOrEmpty(this.PolicyUrl) ? null : new Uri(this.RelyingParty.Channel.GetRequestFromContext().UrlBeforeRewriting, this.Page.ResolveUrl(this.PolicyUrl)), }); } /// /// Creates the relying party instance used to generate authentication requests. /// /// The instantiated relying party. private OpenIdRelyingParty CreateRelyingParty() { // If we're in stateful mode, first use the explicitly given one on this control if there // is one. Then try the configuration file specified one. Finally, use the default // in-memory one that's built into OpenIdRelyingParty. IOpenIdApplicationStore store = this.Stateless ? null : (this.CustomApplicationStore ?? OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.HttpApplicationStore)); var rp = new OpenIdRelyingParty(store); try { // 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; } catch { rp.Dispose(); throw; } } } }