//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Xml; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Reflection; using DotNetOpenAuth.Xrds; using DotNetOpenAuth.Yadis; using Validation; /// /// A trust root to validate requests and match return URLs against. /// /// /// This fills the OpenID Authentication 2.0 specification for realms. /// See http://openid.net/specs/openid-authentication-2_0.html#realms /// [Serializable] [Pure] [DefaultEncoder(typeof(MessagePartRealmConverter))] public class Realm { /// /// A regex used to detect a wildcard that is being used in the realm. /// private const string WildcardDetectionPattern = @"^(\w+://)\*\."; /// /// A (more or less) comprehensive list of top-level (i.e. ".com") domains, /// for use by in order to disallow overly-broad realms /// that allow all web sites ending with '.com', for example. /// private static readonly string[] topLevelDomains = { "com", "edu", "gov", "int", "mil", "net", "org", "biz", "info", "name", "museum", "coop", "aero", "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er", "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gd", "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir", "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru", "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um", "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu", "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw" }; /// /// The Uri of the realm, with the wildcard (if any) removed. /// private Uri uri; /// /// Initializes a new instance of the class. /// /// The realm URL to use in the new instance. [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Not all realms are valid URLs (because of wildcards).")] public Realm(string realmUrl) { Requires.NotNull(realmUrl, "realmUrl"); // not non-zero check so we throw UriFormatException later this.DomainWildcard = Regex.IsMatch(realmUrl, WildcardDetectionPattern); this.uri = new Uri(Regex.Replace(realmUrl, WildcardDetectionPattern, m => m.Groups[1].Value)); if (!this.uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && !this.uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { throw new UriFormatException( string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidScheme, this.uri.Scheme)); } } /// /// Initializes a new instance of the class. /// /// The realm URL of the Relying Party. public Realm(Uri realmUrl) { Requires.NotNull(realmUrl, "realmUrl"); this.uri = realmUrl; if (!this.uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) && !this.uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { throw new UriFormatException( string.Format(CultureInfo.CurrentCulture, OpenIdStrings.InvalidScheme, this.uri.Scheme)); } } /// /// Initializes a new instance of the class. /// /// The realm URI builder. /// /// This is useful because UriBuilder can construct a host with a wildcard /// in the Host property, but once there it can't be converted to a Uri. /// internal Realm(UriBuilder realmUriBuilder) : this(SafeUriBuilderToString(realmUriBuilder)) { } /// /// Gets the suggested realm to use for the calling web application. /// /// A realm that matches this applications root URL. /// /// For most circumstances the Realm generated by this property is sufficient. /// However a wildcard Realm, such as "http://*.microsoft.com/" may at times be more /// desirable than "http://www.microsoft.com/" in order to allow identifier /// correlation across related web sites for directed identity Providers. /// Requires an HttpContext.Current context. /// public static Realm AutoDetect { get { RequiresEx.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); var realmUrl = new UriBuilder(MessagingUtilities.GetWebRoot()); // For RP discovery, the realm url MUST NOT redirect. To prevent this for // virtual directory hosted apps, we need to make sure that the realm path ends // in a slash (since our calculation above guarantees it doesn't end in a specific // page like default.aspx). if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) { realmUrl.Path += "/"; } return realmUrl.Uri; } } /// /// Gets a value indicating whether a '*.' prefix to the hostname is /// used in the realm to allow subdomains or hosts to be added to the URL. /// public bool DomainWildcard { get; private set; } /// /// Gets the host component of this instance. /// public string Host { [DebuggerStepThrough] get { return this.uri.Host; } } /// /// Gets the scheme name for this URI. /// public string Scheme { [DebuggerStepThrough] get { return this.uri.Scheme; } } /// /// Gets the port number of this URI. /// public int Port { [DebuggerStepThrough] get { return this.uri.Port; } } /// /// Gets the absolute path of the URI. /// public string AbsolutePath { [DebuggerStepThrough] get { return this.uri.AbsolutePath; } } /// /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated /// by a question mark (?). /// public string PathAndQuery { [DebuggerStepThrough] get { return this.uri.PathAndQuery; } } /// /// Gets the original string. /// /// The original string. internal string OriginalString { get { return this.uri.OriginalString; } } /// /// Gets the realm URL. If the realm includes a wildcard, it is not included here. /// internal Uri NoWildcardUri { [DebuggerStepThrough] get { return this.uri; } } /// /// Gets the Realm discovery URL, where the wildcard (if present) is replaced with "www.". /// /// /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of /// the "www" prefix. /// internal Uri UriWithWildcardChangedToWww { get { if (this.DomainWildcard) { UriBuilder builder = new UriBuilder(this.NoWildcardUri); builder.Host = "www." + builder.Host; return builder.Uri; } else { return this.NoWildcardUri; } } } /// /// Gets a value indicating whether this realm represents a reasonable (sane) set of URLs. /// /// /// 'http://*.com/', for example is not a reasonable pattern, as it cannot meaningfully /// specify the site claiming it. This function attempts to find many related examples, /// but it can only work via heuristics. Negative responses from this method should be /// treated as advisory, used only to alert the user to examine the trust root carefully. /// internal bool IsSane { get { if (this.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase)) { return true; } string[] host_parts = this.Host.Split('.'); string tld = host_parts[host_parts.Length - 1]; if (Array.IndexOf(topLevelDomains, tld) < 0) { return false; } if (tld.Length == 2) { if (host_parts.Length == 1) { return false; } if (host_parts[host_parts.Length - 2].Length <= 3) { return host_parts.Length > 2; } } else { return host_parts.Length > 1; } return false; } } /// /// Implicitly converts the string-form of a URI to a object. /// /// The URI that the new Realm instance will represent. /// The result of the conversion. [SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "Not all realms are valid URLs (because of wildcards).")] [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Not all Realms are valid URLs.")] [DebuggerStepThrough] public static implicit operator Realm(string uri) { return uri != null ? new Realm(uri) : null; } /// /// Implicitly converts a to a object. /// /// The URI to convert to a realm. /// The result of the conversion. [DebuggerStepThrough] public static implicit operator Realm(Uri uri) { return uri != null ? new Realm(uri) : null; } /// /// Implicitly converts a object to its form. /// /// The realm to convert to a string value. /// The result of the conversion. [DebuggerStepThrough] public static implicit operator string(Realm realm) { return realm != null ? realm.ToString() : null; } /// /// Checks whether one is equal to another. /// /// The to compare with the current . /// /// true if the specified is equal to the current ; otherwise, false. /// /// /// The parameter is null. /// public override bool Equals(object obj) { Realm other = obj as Realm; if (other == null) { return false; } return this.uri.Equals(other.uri) && this.DomainWildcard == other.DomainWildcard; } /// /// Returns the hash code used for storing this object in a hash table. /// /// /// A hash code for the current . /// public override int GetHashCode() { return this.uri.GetHashCode() + (this.DomainWildcard ? 1 : 0); } /// /// Returns the string form of this . /// /// /// A that represents the current . /// public override string ToString() { if (this.DomainWildcard) { UriBuilder builder = new UriBuilder(this.uri); builder.Host = "*." + builder.Host; return builder.ToStringWithImpliedPorts(); } else { return this.uri.AbsoluteUri; } } /// /// Validates a URL against this trust root. /// /// A string specifying URL to check. /// Whether the given URL is within this trust root. internal bool Contains(string url) { return this.Contains(new Uri(url)); } /// /// Validates a URL against this trust root. /// /// The URL to check. /// Whether the given URL is within this trust root. internal bool Contains(Uri url) { if (url.Scheme != this.Scheme) { return false; } if (url.Port != this.Port) { return false; } if (!this.DomainWildcard) { if (url.Host != this.Host) { return false; } } else { Debug.Assert(!string.IsNullOrEmpty(this.Host), "The host part of the Regex should evaluate to at least one char for successful parsed trust roots."); string[] host_parts = this.Host.Split('.'); string[] url_parts = url.Host.Split('.'); // If the domain containing the wildcard has more parts than the URL to match against, // it naturally can't be valid. // Unless *.example.com actually matches example.com too. if (host_parts.Length > url_parts.Length) { return false; } // Compare last part first and move forward. // Maybe could be done by using EndsWith, but piecewies helps ensure that // *.my.com doesn't match ohmeohmy.com but can still match my.com. for (int i = 0; i < host_parts.Length; i++) { string hostPart = host_parts[host_parts.Length - 1 - i]; string urlPart = url_parts[url_parts.Length - 1 - i]; if (!string.Equals(hostPart, urlPart, StringComparison.OrdinalIgnoreCase)) { return false; } } } // If path matches or is specified to root ... // (deliberately case sensitive to protect security on case sensitive systems) if (this.PathAndQuery.Equals(url.PathAndQuery, StringComparison.Ordinal) || this.PathAndQuery.Equals("/", StringComparison.Ordinal)) { return true; } // If trust root has a longer path, the return URL must be invalid. if (this.PathAndQuery.Length > url.PathAndQuery.Length) { return false; } // The following code assures that http://example.com/directory isn't below http://example.com/dir, // but makes sure http://example.com/dir/ectory is below http://example.com/dir int path_len = this.PathAndQuery.Length; string url_prefix = url.PathAndQuery.Substring(0, path_len); if (this.PathAndQuery != url_prefix) { return false; } // If trust root includes a query string ... if (this.PathAndQuery.Contains("?")) { // ... make sure return URL begins with a new argument return url.PathAndQuery[path_len] == '&'; } // Or make sure a query string is introduced or a path below trust root return this.PathAndQuery.EndsWith("/", StringComparison.Ordinal) || url.PathAndQuery[path_len] == '?' || url.PathAndQuery[path_len] == '/'; } /// /// Searches for an XRDS document at the realm URL, and if found, searches /// for a description of a relying party endpoints (OpenId login pages). /// /// The host factories. /// Whether redirects may be followed when discovering the Realm. /// This may be true when creating an unsolicited assertion, but must be /// false when performing return URL verification per 2.0 spec section 9.2.1. /// The cancellation token. /// /// The details of the endpoints if found; or null if no service document was discovered. /// internal virtual async Task> DiscoverReturnToEndpointsAsync(IHostFactories hostFactories, bool allowRedirects, CancellationToken cancellationToken) { XrdsDocument xrds = await this.DiscoverAsync(hostFactories, allowRedirects, cancellationToken); if (xrds != null) { return xrds.FindRelyingPartyReceivingEndpoints(); } return null; } /// /// Searches for an XRDS document at the realm URL. /// /// The host factories. /// Whether redirects may be followed when discovering the Realm. /// This may be true when creating an unsolicited assertion, but must be /// false when performing return URL verification per 2.0 spec section 9.2.1. /// The cancellation token. /// /// The XRDS document if found; or null if no service document was discovered. /// internal virtual async Task DiscoverAsync(IHostFactories hostFactories, bool allowRedirects, CancellationToken cancellationToken) { // Attempt YADIS discovery DiscoveryResult yadisResult = await Yadis.DiscoverAsync(hostFactories, this.UriWithWildcardChangedToWww, false, cancellationToken); if (yadisResult != null) { // Detect disallowed redirects, since realm discovery never allows them for security. ErrorUtilities.VerifyProtocol(allowRedirects || yadisResult.NormalizedUri == yadisResult.RequestUri, OpenIdStrings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri); if (yadisResult.IsXrds) { try { return new XrdsDocument(yadisResult.ResponseText); } catch (XmlException ex) { throw ErrorUtilities.Wrap(ex, XrdsStrings.InvalidXRDSDocument); } } } return null; } /// /// Calls if the argument is non-null. /// Otherwise throws . /// /// The realm URI builder. /// The result of UriBuilder.ToString() /// /// This simple method is worthwhile because it checks for null /// before dereferencing the UriBuilder. Since this is called from /// within a constructor's base(...) call, this avoids a /// when we should be throwing an . /// private static string SafeUriBuilderToString(UriBuilder realmUriBuilder) { Requires.NotNull(realmUriBuilder, "realmUriBuilder"); // Note: we MUST use ToString. Uri property throws if wildcard is present. // Note that Uri.ToString() should generally be avoided, but UriBuilder.ToString() // is safe: http://blog.nerdbank.net/2008/04/uriabsoluteuri-and-uritostring-are-not.html return realmUriBuilder.ToString(); } #if CONTRACTS_FULL /// /// Verifies conditions that should be true for any valid state of this object. /// [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")] [ContractInvariantMethod] private void ObjectInvariant() { Contract.Invariant(this.uri != null); Contract.Invariant(this.uri.AbsoluteUri != null); } #endif /// /// Provides conversions to and from strings for messages that include members of this type. /// private class MessagePartRealmConverter : IMessagePartOriginalEncoder { /// /// Encodes the specified value. /// /// The value. Guaranteed to never be null. /// The in string form, ready for message transport. public string Encode(object value) { Requires.NotNull(value, "value"); return value.ToString(); } /// /// Decodes the specified value. /// /// The string value carried by the transport. Guaranteed to never be null, although it may be empty. /// The deserialized form of the given string. /// Thrown when the string value given cannot be decoded into the required object type. public object Decode(string value) { Requires.NotNull(value, "value"); return new Realm(value); } /// /// Encodes the specified value as the original value that was formerly decoded. /// /// The value. Guaranteed to never be null. /// The in string form, ready for message transport. public string EncodeAsOriginalString(object value) { Requires.NotNull(value, "value"); return ((Realm)value).OriginalString; } } } }