//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // Certain elements are Copyright (c) 2007 Dominick Baier. // //----------------------------------------------------------------------- [assembly: System.Web.UI.WebResource(DotNetOpenAuth.InfoCard.InfoCardSelector.ScriptResourceName, "text/javascript")] namespace DotNetOpenAuth.InfoCard { using System; using System.Collections.ObjectModel; 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; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Xml; using DotNetOpenAuth.Messaging; /// /// The style to use for NOT displaying a hidden region. /// public enum RenderMode { /// /// A hidden region should be invisible while still occupying space in the page layout. /// Static, /// /// A hidden region should collapse so that it does not occupy space in the page layout. /// Dynamic } /// /// An Information Card selector ASP.NET control. /// [ParseChildren(true, "ClaimsRequested")] [PersistChildren(false)] [DefaultEvent("ReceivedToken")] [ToolboxData("<{0}:InfoCardSelector runat=\"server\"><{0}:ClaimType Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier\" />

Your browser does not support Information Cards.

")] [ContractVerification(true)] public class InfoCardSelector : CompositeControl, IPostBackEventHandler { /// /// The resource name for getting at the SupportingScript.js embedded manifest stream. /// internal const string ScriptResourceName = "DotNetOpenAuth.InfoCard.SupportingScript.js"; #region Property constants /// /// Default value for the property. /// private const RenderMode RenderModeDefault = RenderMode.Dynamic; /// /// Default value for the property. /// private const bool AutoPostBackDefault = true; /// /// Default value for the property. /// private const bool AutoPopupDefault = false; /// /// Default value for the property. /// private const string PrivacyUrlDefault = ""; /// /// Default value for the property. /// private const string PrivacyVersionDefault = ""; /// /// Default value for the property. /// private const InfoCardImageSize InfoCardImageDefault = InfoCardImage.DefaultImageSize; /// /// Default value for the property. /// private const string IssuerPolicyDefault = ""; /// /// Default value for the property. /// private const string IssuerDefault = WellKnownIssuers.SelfIssued; /// /// The default value for the property. /// private const string TokenTypeDefault = "urn:oasis:names:tc:SAML:1.0:assertion"; /// /// The viewstate key for storing the property. /// private const string IssuerViewStateKey = "Issuer"; /// /// The viewstate key for storing the property. /// private const string IssuerPolicyViewStateKey = "IssuerPolicy"; /// /// The viewstate key for storing the property. /// private const string AutoPopupViewStateKey = "AutoPopup"; /// /// The viewstate key for storing the property. /// private const string ClaimsRequestedViewStateKey = "ClaimsRequested"; /// /// The viewstate key for storing the property. /// private const string TokenTypeViewStateKey = "TokenType"; /// /// The viewstate key for storing the property. /// private const string PrivacyUrlViewStateKey = "PrivacyUrl"; /// /// The viewstate key for storing the property. /// private const string PrivacyVersionViewStateKey = "PrivacyVersion"; /// /// The viewstate key for storing the property. /// private const string AudienceViewStateKey = "Audience"; /// /// The viewstate key for storing the property. /// private const string AutoPostBackViewStateKey = "AutoPostBack"; /// /// The viewstate key for storing the property. /// private const string ImageSizeViewStateKey = "ImageSize"; /// /// The viewstate key for storing the property. /// private const string RenderModeViewStateKey = "RenderMode"; #endregion #region Categories /// /// The "Behavior" property category. /// private const string BehaviorCategory = "Behavior"; /// /// The "Appearance" property category. /// private const string AppearanceCategory = "Appearance"; /// /// The "InfoCard" property category. /// private const string InfoCardCategory = "InfoCard"; #endregion /// /// The panel containing the controls to display if InfoCard is supported in the user agent. /// private Panel infoCardSupportedPanel; /// /// The panel containing the controls to display if InfoCard is NOT supported in the user agent. /// private Panel infoCardNotSupportedPanel; /// /// Recalls whether the property has been set yet, /// so its default can be set as soon as possible without overwriting /// an intentional value. /// private bool audienceSet; /// /// Initializes a new instance of the class. /// public InfoCardSelector() { this.ToolTip = InfoCardStrings.SelectorClickPrompt; Reporting.RecordFeatureUse(this); } /// /// Occurs when an InfoCard has been submitted but not decoded yet. /// [Category(InfoCardCategory)] public event EventHandler ReceivingToken; /// /// Occurs when an InfoCard has been submitted and decoded. /// [Category(InfoCardCategory)] public event EventHandler ReceivedToken; /// /// Occurs when an InfoCard token is submitted but an error occurs in processing. /// [Category(InfoCardCategory)] public event EventHandler TokenProcessingError; #region Properties /// /// Gets the set of claims that are requested from the Information Card. /// [Description("Specifies the required and optional claims.")] [PersistenceMode(PersistenceMode.InnerProperty), Category(InfoCardCategory)] public Collection ClaimsRequested { get { Contract.Ensures(Contract.Result>() != null); if (this.ViewState[ClaimsRequestedViewStateKey] == null) { var claims = new Collection(); this.ViewState[ClaimsRequestedViewStateKey] = claims; return claims; } else { return (Collection)this.ViewState[ClaimsRequestedViewStateKey]; } } } /// /// Gets or sets the issuer URI. /// [Description("When receiving managed cards, this is the only Issuer whose cards will be accepted.")] [Category(InfoCardCategory), DefaultValue(IssuerDefault)] [TypeConverter(typeof(ComponentModel.IssuersSuggestions))] public string Issuer { get { return (string)this.ViewState[IssuerViewStateKey] ?? IssuerDefault; } set { this.ViewState[IssuerViewStateKey] = value; } } /// /// Gets or sets the issuer policy URI. /// [Description("Specifies the URI of the issuer MEX endpoint")] [Category(InfoCardCategory), DefaultValue(IssuerPolicyDefault)] public string IssuerPolicy { get { return (string)this.ViewState[IssuerPolicyViewStateKey] ?? IssuerPolicyDefault; } set { this.ViewState[IssuerPolicyViewStateKey] = value; } } /// /// Gets or sets the URL to this site's privacy policy. /// [Description("The URL to this site's privacy policy.")] [Category(InfoCardCategory), DefaultValue(PrivacyUrlDefault)] [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "We construct a Uri to validate the format of the string.")] [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "That overload is NOT the same.")] [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "This can take ~/ paths.")] public string PrivacyUrl { get { return (string)this.ViewState[PrivacyUrlViewStateKey] ?? PrivacyUrlDefault; } set { ErrorUtilities.VerifyOperation(string.IsNullOrEmpty(value) || this.Page == null || this.DesignMode || (HttpContext.Current != null && HttpContext.Current.Request != null), MessagingStrings.HttpContextRequired); if (!string.IsNullOrEmpty(value)) { if (this.Page != null && !this.DesignMode) { // Validate new value by trying to construct a Uri based on it. new Uri(new HttpRequestInfo(HttpContext.Current.Request).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[PrivacyUrlViewStateKey] = value; } } /// /// Gets or sets the version of the privacy policy file. /// [Description("Specifies the version of the privacy policy file")] [Category(InfoCardCategory), DefaultValue(PrivacyVersionDefault)] public string PrivacyVersion { get { return (string)this.ViewState[PrivacyVersionViewStateKey] ?? PrivacyVersionDefault; } set { this.ViewState[PrivacyVersionViewStateKey] = value; } } /// /// Gets or sets the URI that must be found for the SAML token's intended audience /// in order for the token to be processed. /// /// Typically the URI of the page hosting the control, or null to disable audience verification. /// /// Disabling audience verification introduces a security risk /// because tokens can be redirected to allow access to unintended resources. /// [Description("Specifies the URI that must be found for the SAML token's intended audience.")] [Bindable(true), Category(InfoCardCategory)] [TypeConverter(typeof(ComponentModel.UriConverter))] [UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] public Uri Audience { get { return (Uri)this.ViewState[AudienceViewStateKey]; } set { this.ViewState[AudienceViewStateKey] = value; this.audienceSet = true; } } /// /// Gets or sets a value indicating whether a postback will automatically /// be invoked when the user selects an Information Card. /// [Description("Specifies if the pages automatically posts back after the user has selected a card")] [Category(BehaviorCategory), DefaultValue(AutoPostBackDefault)] public bool AutoPostBack { get { return (bool)(this.ViewState[AutoPostBackViewStateKey] ?? AutoPostBackDefault); } set { this.ViewState[AutoPostBackViewStateKey] = value; } } /// /// Gets or sets the size of the standard InfoCard image to display. /// /// The default size is 114x80. [Description("The size of the InfoCard image to use. Defaults to 114x80.")] [DefaultValue(InfoCardImageDefault), Category(AppearanceCategory)] public InfoCardImageSize ImageSize { get { return (InfoCardImageSize)(this.ViewState[ImageSizeViewStateKey] ?? InfoCardImageDefault); } set { this.ViewState[ImageSizeViewStateKey] = value; } } /// /// Gets or sets the template to display when the user agent lacks /// an Information Card selector. /// [Browsable(false), DefaultValue("")] [PersistenceMode(PersistenceMode.InnerProperty), TemplateContainer(typeof(InfoCardSelector))] public virtual ITemplate UnsupportedTemplate { get; set; } /// /// Gets or sets a value indicating whether a hidden region (either /// the unsupported or supported InfoCard HTML) /// collapses or merely becomes invisible when it is not to be displayed. /// [Description("Whether the hidden region collapses or merely becomes invisible.")] [Category(AppearanceCategory), DefaultValue(RenderModeDefault)] public RenderMode RenderMode { get { return (RenderMode)(this.ViewState[RenderModeViewStateKey] ?? RenderModeDefault); } set { this.ViewState[RenderModeViewStateKey] = value; } } /// /// Gets or sets a value indicating whether the identity selector will be triggered at page load. /// [Description("Controls whether the InfoCard selector automatically appears when the page is loaded.")] [Category(BehaviorCategory), DefaultValue(AutoPopupDefault)] public bool AutoPopup { get { return (bool)(this.ViewState[AutoPopupViewStateKey] ?? AutoPopupDefault); } set { this.ViewState[AutoPopupViewStateKey] = value; } } #endregion /// /// Gets the name of the hidden field that is used to transport the token back to the server. /// private string HiddenFieldName { get { return this.ClientID + "_tokenxml"; } } /// /// Gets the id of the OBJECT tag that creates the InfoCard Selector. /// private string SelectorObjectId { get { return this.ClientID + "_cs"; } } /// /// Gets the XML token, which will be encrypted if it was received over SSL. /// private string TokenXml { get { return this.Page.Request.Form[this.HiddenFieldName]; } } /// /// Gets or sets the type of token the page is prepared to receive. /// [Description("Specifies the token type. Defaults to SAML 1.0")] [DefaultValue(TokenTypeDefault), Category(InfoCardCategory)] private string TokenType { get { return (string)this.ViewState[TokenTypeViewStateKey] ?? TokenTypeDefault; } set { this.ViewState[TokenTypeViewStateKey] = value; } } /// /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server. /// /// A that represents an optional event argument to be passed to the event handler. void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) { this.RaisePostBackEvent(eventArgument); } /// /// When implemented by a class, enables a server control to process an event raised when a form is posted to the server. /// /// A that represents an optional event argument to be passed to the event handler. [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "Predefined signature.")] protected virtual void RaisePostBackEvent(string eventArgument) { if (!string.IsNullOrEmpty(this.TokenXml)) { try { ReceivingTokenEventArgs receivingArgs = this.OnReceivingToken(this.TokenXml); if (!receivingArgs.Cancel) { try { Token token = Token.Read(this.TokenXml, this.Audience, receivingArgs.DecryptingTokens); this.OnReceivedToken(token); } catch (InformationCardException ex) { this.OnTokenProcessingError(this.TokenXml, ex); } } } catch (XmlException ex) { this.OnTokenProcessingError(this.TokenXml, ex); } } } /// /// Fires the event. /// /// The token XML, prior to any processing. /// The event arguments sent to the event handlers. [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "decryptor", Justification = "By design")] protected virtual ReceivingTokenEventArgs OnReceivingToken(string tokenXml) { Requires.NotNull(tokenXml, "tokenXml"); var args = new ReceivingTokenEventArgs(tokenXml); var receivingToken = this.ReceivingToken; if (receivingToken != null) { receivingToken(this, args); } return args; } /// /// Fires the event. /// /// The token, if it was decrypted. protected virtual void OnReceivedToken(Token token) { Requires.NotNull(token, "token"); var receivedInfoCard = this.ReceivedToken; if (receivedInfoCard != null) { receivedInfoCard(this, new ReceivedTokenEventArgs(token)); } } /// /// Fires the event. /// /// The unprocessed token. /// The exception generated while processing the token. protected virtual void OnTokenProcessingError(string unprocessedToken, Exception ex) { Requires.NotNull(unprocessedToken, "unprocessedToken"); Requires.NotNull(ex, "ex"); var tokenProcessingError = this.TokenProcessingError; if (tokenProcessingError != null) { TokenProcessingErrorEventArgs args = new TokenProcessingErrorEventArgs(unprocessedToken, ex); tokenProcessingError(this, args); } } /// /// Raises the event. /// /// An object that contains the event data. protected override void OnInit(EventArgs e) { // Give a default for the Audience property that allows for // the aspx page to have preset it, and ViewState // to initialize it (even to null) after this. if (!this.audienceSet && !this.DesignMode) { this.Audience = this.Page.Request.Url; } base.OnInit(e); this.Page.LoadComplete += delegate { this.EnsureChildControls(); }; } /// /// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering. /// protected override void CreateChildControls() { base.CreateChildControls(); this.Page.ClientScript.RegisterHiddenField(this.HiddenFieldName, string.Empty); this.Controls.Add(this.infoCardSupportedPanel = this.CreateInfoCardSupportedPanel()); this.Controls.Add(this.infoCardNotSupportedPanel = this.CreateInfoCardUnsupportedPanel()); this.RenderSupportingScript(); } /// /// Raises the event. /// /// An object that contains the event data. protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); if (!this.DesignMode) { // The Cardspace selector will display an ugly error to the user if // the privacy URL is present but the privacy version is not. ErrorUtilities.VerifyOperation(string.IsNullOrEmpty(this.PrivacyUrl) || !string.IsNullOrEmpty(this.PrivacyVersion), InfoCardStrings.PrivacyVersionRequiredWithPrivacyUrl); } this.RegisterInfoCardSelectorObjectScript(); } /// /// Creates a control that renders to <Param Name="{0}" Value="{1}" /> /// /// The parameter name. /// The parameter value. /// The control that renders to the Param tag. private static string CreateParamJs(string name, string value) { Contract.Ensures(Contract.Result() != null); string scriptFormat = @" objp = document.createElement('param'); objp.name = {0}; objp.value = {1}; obj.appendChild(objp); "; return string.Format( CultureInfo.InvariantCulture, scriptFormat, MessagingUtilities.GetSafeJavascriptValue(name), MessagingUtilities.GetSafeJavascriptValue(value)); } /// /// Creates the panel whose contents are displayed to the user /// on a user agent that has an Information Card selector. /// /// The Panel control [Pure] private Panel CreateInfoCardSupportedPanel() { Contract.Ensures(Contract.Result() != null); Panel supportedPanel = new Panel(); try { if (!this.DesignMode) { // At the user agent, assume InfoCard is not supported until // the JavaScript discovers otherwise and reveals this panel. supportedPanel.Style[HtmlTextWriterStyle.Display] = "none"; } supportedPanel.Controls.Add(this.CreateInfoCardImage()); // trigger the selector at page load? if (this.AutoPopup && !this.Page.IsPostBack) { this.Page.ClientScript.RegisterStartupScript( typeof(InfoCardSelector), "selector_load_trigger", this.GetInfoCardSelectorActivationScript(true), true); } return supportedPanel; } catch { supportedPanel.Dispose(); throw; } } /// /// Gets the InfoCard selector activation script. /// /// Whether a postback should always immediately follow the selector, even if is false. /// The javascript to inject into the surrounding context. private string GetInfoCardSelectorActivationScript(bool alwaysPostback) { // generate call do __doPostback PostBackOptions options = new PostBackOptions(this); string postback = string.Empty; if (alwaysPostback || this.AutoPostBack) { postback = this.Page.ClientScript.GetPostBackEventReference(options) + ";"; } // generate the onclick script for the image string invokeScript = string.Format( CultureInfo.InvariantCulture, @"if (document.infoCard.activate('{0}', '{1}')) {{ {2} }}", this.SelectorObjectId, this.HiddenFieldName, postback); return invokeScript; } /// /// Creates the panel whose contents are displayed to the user /// on a user agent that does not have an Information Card selector. /// /// The Panel control. [Pure] private Panel CreateInfoCardUnsupportedPanel() { Contract.Ensures(Contract.Result() != null); Panel unsupportedPanel = new Panel(); try { if (this.UnsupportedTemplate != null) { this.UnsupportedTemplate.InstantiateIn(unsupportedPanel); } return unsupportedPanel; } catch { unsupportedPanel.Dispose(); throw; } } /// /// Adds the javascript that adds the info card selector <object> HTML tag to the page. /// [Pure] private void RegisterInfoCardSelectorObjectScript() { string scriptFormat = @"{{ var obj = document.createElement('object'); obj.type = 'application/x-informationcard'; obj.id = {0}; obj.style.display = 'none'; "; StringBuilder script = new StringBuilder(); script.AppendFormat( CultureInfo.InvariantCulture, scriptFormat, MessagingUtilities.GetSafeJavascriptValue(this.ClientID + "_cs")); if (!string.IsNullOrEmpty(this.Issuer)) { script.AppendLine(CreateParamJs("issuer", this.Issuer)); } if (!string.IsNullOrEmpty(this.IssuerPolicy)) { script.AppendLine(CreateParamJs("issuerPolicy", this.IssuerPolicy)); } if (!string.IsNullOrEmpty(this.TokenType)) { script.AppendLine(CreateParamJs("tokenType", this.TokenType)); } string requiredClaims, optionalClaims; this.GetRequestedClaims(out requiredClaims, out optionalClaims); ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(requiredClaims) || !string.IsNullOrEmpty(optionalClaims), InfoCardStrings.EmptyClaimListNotAllowed); if (!string.IsNullOrEmpty(requiredClaims)) { script.AppendLine(CreateParamJs("requiredClaims", requiredClaims)); } if (!string.IsNullOrEmpty(optionalClaims)) { script.AppendLine(CreateParamJs("optionalClaims", optionalClaims)); } if (!string.IsNullOrEmpty(this.PrivacyUrl)) { string privacyUrl = this.DesignMode ? this.PrivacyUrl : new Uri(Page.Request.Url, Page.ResolveUrl(this.PrivacyUrl)).AbsoluteUri; script.AppendLine(CreateParamJs("privacyUrl", privacyUrl)); } if (!string.IsNullOrEmpty(this.PrivacyVersion)) { script.AppendLine(CreateParamJs("privacyVersion", this.PrivacyVersion)); } script.AppendLine(@"if (document.infoCard.isSupported()) { document.write(obj.outerHTML); } }"); this.Page.ClientScript.RegisterClientScriptBlock(typeof(InfoCardSelector), this.ClientID + "tag", script.ToString(), true); } /// /// Creates the info card clickable image. /// /// An Image object. [Pure] private Image CreateInfoCardImage() { // add clickable image Image image = new Image(); try { image.ImageUrl = this.Page.ClientScript.GetWebResourceUrl(typeof(InfoCardSelector), InfoCardImage.GetImageManifestResourceStreamName(this.ImageSize)); image.AlternateText = InfoCardStrings.SelectorClickPrompt; image.ToolTip = this.ToolTip; image.Style[HtmlTextWriterStyle.Cursor] = "hand"; image.Attributes["onclick"] = this.GetInfoCardSelectorActivationScript(false); return image; } catch { image.Dispose(); throw; } } /// /// Compiles lists of requested/required claims that should accompany /// any submitted Information Card. /// /// A space-delimited list of claim type URIs for claims that must be included in a submitted Information Card. /// A space-delimited list of claim type URIs for claims that may optionally be included in a submitted Information Card. [Pure] private void GetRequestedClaims(out string required, out string optional) { Requires.ValidState(this.ClaimsRequested != null); Contract.Ensures(Contract.ValueAtReturn(out required) != null); Contract.Ensures(Contract.ValueAtReturn(out optional) != null); var nonEmptyClaimTypes = this.ClaimsRequested.Where(c => c.Name != null); var optionalClaims = from claim in nonEmptyClaimTypes where claim.IsOptional select claim.Name; var requiredClaims = from claim in nonEmptyClaimTypes where !claim.IsOptional select claim.Name; string[] requiredClaimsArray = requiredClaims.ToArray(); string[] optionalClaimsArray = optionalClaims.ToArray(); required = string.Join(" ", requiredClaimsArray); optional = string.Join(" ", optionalClaimsArray); Contract.Assume(required != null); Contract.Assume(optional != null); } /// /// Adds Javascript snippets to the page to help the Information Card selector do its work, /// or to downgrade gracefully if the user agent lacks an Information Card selector. /// private void RenderSupportingScript() { Requires.ValidState(this.infoCardSupportedPanel != null); this.Page.ClientScript.RegisterClientScriptResource(typeof(InfoCardSelector), ScriptResourceName); if (this.RenderMode == RenderMode.Static) { this.Page.ClientScript.RegisterStartupScript( typeof(InfoCardSelector), "SelectorSupportingScript_" + this.ClientID, string.Format(CultureInfo.InvariantCulture, "document.infoCard.checkStatic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID), true); } else if (RenderMode == RenderMode.Dynamic) { this.Page.ClientScript.RegisterStartupScript( typeof(InfoCardSelector), "SelectorSupportingScript_" + this.ClientID, string.Format(CultureInfo.InvariantCulture, "document.infoCard.checkDynamic('{0}', '{1}');", this.infoCardSupportedPanel.ClientID, this.infoCardNotSupportedPanel.ClientID), true); } } } }