summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/Realm.cs')
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Realm.cs512
1 files changed, 512 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs b/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs
new file mode 100644
index 0000000..685b922
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs
@@ -0,0 +1,512 @@
+//-----------------------------------------------------------------------
+// <copyright file="Realm.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+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.Text.RegularExpressions;
+ using System.Web;
+ using System.Xml;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.Xrds;
+ using DotNetOpenAuth.Yadis;
+
+ /// <summary>
+ /// A trust root to validate requests and match return URLs against.
+ /// </summary>
+ /// <remarks>
+ /// This fills the OpenID Authentication 2.0 specification for realms.
+ /// See http://openid.net/specs/openid-authentication-2_0.html#realms
+ /// </remarks>
+ [Serializable]
+ [Pure]
+ public class Realm {
+ /// <summary>
+ /// A regex used to detect a wildcard that is being used in the realm.
+ /// </summary>
+ private const string WildcardDetectionPattern = @"^(\w+://)\*\.";
+
+ /// <summary>
+ /// A (more or less) comprehensive list of top-level (i.e. ".com") domains,
+ /// for use by <see cref="IsSane"/> in order to disallow overly-broad realms
+ /// that allow all web sites ending with '.com', for example.
+ /// </summary>
+ 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" };
+
+ /// <summary>
+ /// The Uri of the realm, with the wildcard (if any) removed.
+ /// </summary>
+ private Uri uri;
+
+ /// <summary>
+ /// Initializes static members of the <see cref="Realm"/> class.
+ /// </summary>
+ static Realm() {
+ Func<string, Realm> safeRealm = str => {
+ Contract.Assume(str != null);
+ return new Realm(str);
+ };
+ MessagePart.Map<Realm>(realm => realm.ToString(), realm => realm.OriginalString, safeRealm);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Realm"/> class.
+ /// </summary>
+ /// <param name="realmUrl">The realm URL to use in the new instance.</param>
+ [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));
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Realm"/> class.
+ /// </summary>
+ /// <param name="realmUrl">The realm URL of the Relying Party.</param>
+ 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));
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Realm"/> class.
+ /// </summary>
+ /// <param name="realmUriBuilder">The realm URI builder.</param>
+ /// <remarks>
+ /// 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.
+ /// </remarks>
+ internal Realm(UriBuilder realmUriBuilder)
+ : this(SafeUriBuilderToString(realmUriBuilder)) { }
+
+ /// <summary>
+ /// Gets the suggested realm to use for the calling web application.
+ /// </summary>
+ /// <value>A realm that matches this applications root URL.</value>
+ /// <remarks>
+ /// <para>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.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ public static Realm AutoDetect {
+ get {
+ Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Ensures(Contract.Result<Realm>() != null);
+
+ HttpRequestInfo requestInfo = new HttpRequestInfo(HttpContext.Current.Request);
+ UriBuilder realmUrl = new UriBuilder(requestInfo.UrlBeforeRewriting);
+ realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
+ realmUrl.Query = null;
+ realmUrl.Fragment = null;
+
+ // For RP discovery, the realm url MUST NOT redirect. To prevent this for
+ // virtual directory hosted apps, we need to make sure that the realm path ends
+ // in a slash (since our calculation above guarantees it doesn't end in a specific
+ // page like default.aspx).
+ if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) {
+ realmUrl.Path += "/";
+ }
+
+ return realmUrl.Uri;
+ }
+ }
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ public bool DomainWildcard { get; private set; }
+
+ /// <summary>
+ /// Gets the host component of this instance.
+ /// </summary>
+ public string Host {
+ [DebuggerStepThrough]
+ get { return this.uri.Host; }
+ }
+
+ /// <summary>
+ /// Gets the scheme name for this URI.
+ /// </summary>
+ public string Scheme {
+ [DebuggerStepThrough]
+ get { return this.uri.Scheme; }
+ }
+
+ /// <summary>
+ /// Gets the port number of this URI.
+ /// </summary>
+ public int Port {
+ [DebuggerStepThrough]
+ get { return this.uri.Port; }
+ }
+
+ /// <summary>
+ /// Gets the absolute path of the URI.
+ /// </summary>
+ public string AbsolutePath {
+ [DebuggerStepThrough]
+ get { return this.uri.AbsolutePath; }
+ }
+
+ /// <summary>
+ /// Gets the System.Uri.AbsolutePath and System.Uri.Query properties separated
+ /// by a question mark (?).
+ /// </summary>
+ public string PathAndQuery {
+ [DebuggerStepThrough]
+ get { return this.uri.PathAndQuery; }
+ }
+
+ /// <summary>
+ /// Gets the original string.
+ /// </summary>
+ /// <value>The original string.</value>
+ internal string OriginalString {
+ get { return this.uri.OriginalString; }
+ }
+
+ /// <summary>
+ /// Gets the realm URL. If the realm includes a wildcard, it is not included here.
+ /// </summary>
+ internal Uri NoWildcardUri {
+ [DebuggerStepThrough]
+ get { return this.uri; }
+ }
+
+ /// <summary>
+ /// Gets the Realm discovery URL, where the wildcard (if present) is replaced with "www.".
+ /// </summary>
+ /// <remarks>
+ /// See OpenID 2.0 spec section 9.2.1 for the explanation on the addition of
+ /// the "www" prefix.
+ /// </remarks>
+ 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;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this realm represents a reasonable (sane) set of URLs.
+ /// </summary>
+ /// <remarks>
+ /// '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.
+ /// </remarks>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Implicitly converts the string-form of a URI to a <see cref="Realm"/> object.
+ /// </summary>
+ /// <param name="uri">The URI that the new Realm instance will represent.</param>
+ /// <returns>The result of the conversion.</returns>
+ [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) {
+ Contract.Ensures((Contract.Result<Realm>() != null) == (uri != null));
+ return uri != null ? new Realm(uri) : null;
+ }
+
+ /// <summary>
+ /// Implicitly converts a <see cref="Uri"/> to a <see cref="Realm"/> object.
+ /// </summary>
+ /// <param name="uri">The URI to convert to a realm.</param>
+ /// <returns>The result of the conversion.</returns>
+ [DebuggerStepThrough]
+ public static implicit operator Realm(Uri uri) {
+ Contract.Ensures((Contract.Result<Realm>() != null) == (uri != null));
+ return uri != null ? new Realm(uri) : null;
+ }
+
+ /// <summary>
+ /// Implicitly converts a <see cref="Realm"/> object to its <see cref="String"/> form.
+ /// </summary>
+ /// <param name="realm">The realm to convert to a string value.</param>
+ /// <returns>The result of the conversion.</returns>
+ [DebuggerStepThrough]
+ public static implicit operator string(Realm realm) {
+ Contract.Ensures((Contract.Result<string>() != null) == (realm != null));
+ return realm != null ? realm.ToString() : null;
+ }
+
+ /// <summary>
+ /// Checks whether one <see cref="Realm"/> is equal to another.
+ /// </summary>
+ /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
+ /// <returns>
+ /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
+ /// </returns>
+ /// <exception cref="T:System.NullReferenceException">
+ /// The <paramref name="obj"/> parameter is null.
+ /// </exception>
+ 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;
+ }
+
+ /// <summary>
+ /// Returns the hash code used for storing this object in a hash table.
+ /// </summary>
+ /// <returns>
+ /// A hash code for the current <see cref="T:System.Object"/>.
+ /// </returns>
+ public override int GetHashCode() {
+ return this.uri.GetHashCode() + (this.DomainWildcard ? 1 : 0);
+ }
+
+ /// <summary>
+ /// Returns the string form of this <see cref="Realm"/>.
+ /// </summary>
+ /// <returns>
+ /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
+ /// </returns>
+ 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;
+ }
+ }
+
+ /// <summary>
+ /// Validates a URL against this trust root.
+ /// </summary>
+ /// <param name="url">A string specifying URL to check.</param>
+ /// <returns>Whether the given URL is within this trust root.</returns>
+ internal bool Contains(string url) {
+ return this.Contains(new Uri(url));
+ }
+
+ /// <summary>
+ /// Validates a URL against this trust root.
+ /// </summary>
+ /// <param name="url">The URL to check.</param>
+ /// <returns>Whether the given URL is within this trust root.</returns>
+ 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] == '/';
+ }
+
+ /// <summary>
+ /// Searches for an XRDS document at the realm URL, and if found, searches
+ /// for a description of a relying party endpoints (OpenId login pages).
+ /// </summary>
+ /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param>
+ /// <param name="allowRedirects">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.</param>
+ /// <returns>
+ /// The details of the endpoints if found; or <c>null</c> if no service document was discovered.
+ /// </returns>
+ internal virtual IEnumerable<RelyingPartyEndpointDescription> DiscoverReturnToEndpoints(IDirectWebRequestHandler requestHandler, bool allowRedirects) {
+ XrdsDocument xrds = this.Discover(requestHandler, allowRedirects);
+ if (xrds != null) {
+ return xrds.FindRelyingPartyReceivingEndpoints();
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Searches for an XRDS document at the realm URL.
+ /// </summary>
+ /// <param name="requestHandler">The mechanism to use for sending HTTP requests.</param>
+ /// <param name="allowRedirects">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.</param>
+ /// <returns>
+ /// The XRDS document if found; or <c>null</c> if no service document was discovered.
+ /// </returns>
+ internal virtual XrdsDocument Discover(IDirectWebRequestHandler requestHandler, bool allowRedirects) {
+ // Attempt YADIS discovery
+ DiscoveryResult yadisResult = Yadis.Discover(requestHandler, this.UriWithWildcardChangedToWww, false);
+ 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;
+ }
+
+ /// <summary>
+ /// Calls <see cref="UriBuilder.ToString"/> if the argument is non-null.
+ /// Otherwise throws <see cref="ArgumentNullException"/>.
+ /// </summary>
+ /// <param name="realmUriBuilder">The realm URI builder.</param>
+ /// <returns>The result of UriBuilder.ToString()</returns>
+ /// <remarks>
+ /// 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 <see cref="NullReferenceException"/>
+ /// when we should be throwing an <see cref="ArgumentNullException"/>.
+ /// </remarks>
+ 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
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [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
+ }
+}