//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.ChannelElements;
using DotNetOpenAuth.OpenId.Extensions;
using Org.Mentalis.Security.Cryptography;
///
/// A set of utilities especially useful to OpenID.
///
public static class OpenIdUtilities {
///
/// The prefix to designate this library's proprietary parameters added to the protocol.
///
internal const string CustomParameterPrefix = "dnoa.";
///
/// A static variable that carries the results of a check for the presence of
/// assemblies that are required for the Diffie-Hellman algorithm.
///
private static bool? diffieHellmanPresent;
///
/// Gets a value indicating whether Diffie Hellman is available in this installation.
///
///
/// true if Diffie-Hellman functionality is present; otherwise, false.
///
internal static bool IsDiffieHellmanPresent {
get {
if (!diffieHellmanPresent.HasValue) {
try {
LoadDiffieHellmanTypes();
diffieHellmanPresent = true;
} catch (FileNotFoundException) {
diffieHellmanPresent = false;
} catch (TypeLoadException) {
diffieHellmanPresent = false;
}
if (diffieHellmanPresent.Value) {
Logger.OpenId.Info("Diffie-Hellman supporting assemblies found and loaded.");
} else {
Logger.OpenId.Warn("Diffie-Hellman supporting assemblies failed to load. Only associations with HTTPS OpenID Providers will be supported.");
}
}
return diffieHellmanPresent.Value;
}
}
///
/// Creates a random association handle.
///
/// The association handle.
public static string GenerateRandomAssociationHandle() {
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result()));
// Generate the handle. It must be unique, and preferably unpredictable,
// so we use a time element and a random data element to generate it.
string uniq = MessagingUtilities.GetCryptoRandomDataAsBase64(4);
return string.Format(CultureInfo.InvariantCulture, "{{{0}}}{{{1}}}", DateTime.UtcNow.Ticks, uniq);
}
///
/// Gets the OpenID protocol instance for the version in a message.
///
/// The message.
/// The OpenID protocol instance.
internal static Protocol GetProtocol(this IProtocolMessage message) {
Requires.NotNull(message, "message");
return Protocol.Lookup(message.Version);
}
///
/// Changes the position of some element in a list.
///
/// The type of elements stored in the list.
/// The list to be modified.
/// The new position for the given element.
/// The element to move within the list.
/// Thrown if the element does not already exist in the list.
internal static void MoveTo(this IList list, int position, T value) {
ErrorUtilities.VerifyInternal(list.Remove(value), "Unable to find element in list.");
list.Insert(position, value);
}
///
/// Corrects any URI decoding the Provider may have inappropriately done
/// to our return_to URL, resulting in an otherwise corrupted base64 encoded value.
///
/// The base64 encoded value. May be null.
///
/// The value; corrected if corruption had occurred.
///
///
/// AOL may have incorrectly URI-decoded the token for us in the return_to,
/// resulting in a token URI-decoded twice by the time we see it, and no
/// longer being a valid base64 string.
/// It turns out that the only symbols from base64 that is also encoded
/// in URI encoding rules are the + and / characters.
/// AOL decodes the %2b sequence to the + character
/// and the %2f sequence to the / character (it shouldn't decode at all).
/// When we do our own URI decoding, the + character becomes a space (corrupting base64)
/// but the / character remains a /, so no further corruption happens to this character.
/// So to correct this we just need to change any spaces we find in the token
/// back to + characters.
///
internal static string FixDoublyUriDecodedBase64String(string value) {
if (value == null) {
return null;
}
if (value.Contains(" ")) {
Logger.OpenId.Error("Deserializing a corrupted token. The OpenID Provider may have inappropriately decoded the return_to URL before sending it back to us.");
value = value.Replace(' ', '+'); // Undo any extra decoding the Provider did
}
return value;
}
///
/// Rounds the given downward to the whole second.
///
/// The DateTime object to adjust.
/// The new value.
internal static DateTime CutToSecond(DateTime dateTime) {
return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind);
}
///
/// Gets the fully qualified Realm URL, given a Realm that may be relative to a particular page.
///
/// The hosting page that has the realm value to resolve.
/// The realm, which may begin with "*." or "~/".
/// The request context.
/// The fully-qualified realm.
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")]
internal static UriBuilder GetResolvedRealm(Page page, string realm, HttpRequestBase requestContext) {
Requires.NotNull(page, "page");
Requires.NotNull(requestContext, "requestContext");
// Allow for *. realm notation, as well as ASP.NET ~/ shortcuts.
// We have to temporarily remove the *. notation if it's there so that
// the rest of our URL manipulation will succeed.
bool foundWildcard = false;
// Note: we don't just use string.Replace because poorly written URLs
// could potentially have multiple :// sequences in them.
MatchEvaluator matchDelegate = delegate(Match m) {
foundWildcard = true;
return m.Groups[1].Value;
};
string realmNoWildcard = Regex.Replace(realm, @"^(\w+://)\*\.", matchDelegate);
UriBuilder fullyQualifiedRealm = new UriBuilder(
new Uri(requestContext.GetPublicFacingUrl(), page.ResolveUrl(realmNoWildcard)));
if (foundWildcard) {
fullyQualifiedRealm.Host = "*." + fullyQualifiedRealm.Host;
}
// Is it valid?
new Realm(fullyQualifiedRealm); // throws if not valid
return fullyQualifiedRealm;
}
///
/// Gets the extension factories from the extension aggregator on an OpenID channel.
///
/// The channel.
/// The list of factories that will be used to generate extension instances.
///
/// This is an extension method on rather than an instance
/// method on because the OpenIdRelyingParty
/// and OpenIdProvider classes don't strong-type to
/// to allow flexibility in the specific type of channel the user (or tests)
/// can plug in.
///
internal static IList GetExtensionFactories(this Channel channel) {
Requires.NotNull(channel, "channel");
var extensionsBindingElement = channel.BindingElements.OfType().SingleOrDefault();
ErrorUtilities.VerifyOperation(extensionsBindingElement != null, OpenIdStrings.UnsupportedChannelConfiguration);
IOpenIdExtensionFactory factory = extensionsBindingElement.ExtensionFactory;
var aggregator = factory as OpenIdExtensionFactoryAggregator;
ErrorUtilities.VerifyOperation(aggregator != null, OpenIdStrings.UnsupportedChannelConfiguration);
return aggregator.Factories;
}
///
/// Loads the Diffie-Hellman assemblies.
///
/// Thrown if the DH assemblies are missing.
private static void LoadDiffieHellmanTypes() {
// This seeming no-op instruction is enough for the CLR to throw a FileNotFoundException
// If the assemblies are missing.
new DiffieHellmanManaged();
}
}
}