//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OpenId.Provider {
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Drawing.Design;
using System.Web.UI;
using DotNetOpenAuth.Messaging;
///
/// An ASP.NET control that manages the OpenID identity advertising tags
/// of a user's Identity Page that allow a relying party web site to discover
/// how to authenticate a user.
///
[DefaultProperty("ServerUrl")]
[ToolboxData("<{0}:IdentityEndpoint runat=\"server\" ProviderEndpointUrl=\"\" />")]
public class IdentityEndpoint : XrdsPublisher {
#region Property viewstate keys
///
/// The viewstate key to use for storing the value of the property.
///
private const string AutoNormalizeRequestViewStateKey = "AutoNormalizeRequest";
///
/// The viewstate key to use for storing the value of the property.
///
private const string ProviderLocalIdentifierViewStateKey = "ProviderLocalIdentifier";
///
/// The viewstate key to use for storing the value of the property.
///
private const string ProviderVersionViewStateKey = "ProviderVersion";
///
/// The viewstate key to use for storing the value of the property.
///
private const string ProviderEndpointUrlViewStateKey = "ProviderEndpointUrl";
#endregion
///
/// The default value for the property.
///
private const ProtocolVersion ProviderVersionDefault = ProtocolVersion.V20;
///
/// Initializes a new instance of the class.
///
public IdentityEndpoint() {
}
///
/// Fired at each page request so the host web site can return the normalized
/// version of the request URI.
///
public event EventHandler NormalizeUri;
#region Properties
///
/// Gets or sets the OpenID version supported by the provider.
/// If multiple versions are supported, this should be set to the latest
/// version that this library and the Provider both support.
///
[Category("Behavior")]
[DefaultValue(ProviderVersionDefault)]
[Description("The OpenID version supported by the provider.")]
public ProtocolVersion ProviderVersion {
get {
return this.ViewState[ProviderVersionViewStateKey] == null ?
ProviderVersionDefault : (ProtocolVersion)this.ViewState[ProviderVersionViewStateKey];
}
set {
this.ViewState[ProviderVersionViewStateKey] = value;
}
}
///
/// Gets or sets the Provider URL that processes OpenID requests.
///
[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Forms designer property grid only supports primitive types.")]
[Bindable(true), Category("Behavior")]
[Description("The Provider URL that processes OpenID requests.")]
[UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string ProviderEndpointUrl {
get {
return (string)ViewState[ProviderEndpointUrlViewStateKey];
}
set {
UriUtil.ValidateResolvableUrl(Page, DesignMode, value);
ViewState[ProviderEndpointUrlViewStateKey] = value;
}
}
///
/// Gets or sets the Identifier that is controlled by the Provider.
///
[Bindable(true)]
[Category("Behavior")]
[Description("The user Identifier that is controlled by the Provider.")]
public string ProviderLocalIdentifier {
get {
return (string)ViewState[ProviderLocalIdentifierViewStateKey];
}
set {
UriUtil.ValidateResolvableUrl(Page, DesignMode, value);
ViewState[ProviderLocalIdentifierViewStateKey] = value;
}
}
///
/// Gets or sets a value indicating whether every incoming request
/// will be checked for normalized form and redirected if it is not.
///
///
/// If set to true (and it should be), you should also handle the
/// event and apply your own policy for normalizing the URI.
/// If multiple controls are on a single page (to support
/// multiple versions of OpenID for example) then only one of them should have this
/// property set to true.
///
[Bindable(true)]
[Category("Behavior")]
[Description("Whether every incoming request will be checked for normalized form and redirected if it is not. If set to true, consider handling the NormalizeUri event.")]
public bool AutoNormalizeRequest {
get { return (bool)(ViewState[AutoNormalizeRequestViewStateKey] ?? false); }
set { ViewState[AutoNormalizeRequestViewStateKey] = value; }
}
#endregion
///
/// Gets the protocol to use for advertising OpenID on the identity page.
///
internal Protocol Protocol {
get { return Protocol.Lookup(this.ProviderVersion); }
}
///
/// Checks the incoming request and invokes a browser redirect if the URL has not been normalized.
///
///
protected virtual void OnNormalize() {
UriIdentifier userSuppliedIdentifier = MessagingUtilities.GetRequestUrlFromContext();
var normalizationArgs = new IdentityEndpointNormalizationEventArgs(userSuppliedIdentifier);
var normalizeUri = this.NormalizeUri;
if (normalizeUri != null) {
normalizeUri(this, normalizationArgs);
} else {
// Do some best-guess normalization.
normalizationArgs.NormalizedIdentifier = BestGuessNormalization(normalizationArgs.UserSuppliedIdentifier);
}
// If we have a normalized form, we should use it.
// We compare path and query with case sensitivity and host name without case sensitivity deliberately,
// and the fragment will be asserted or cleared by the OP during authentication.
if (normalizationArgs.NormalizedIdentifier != null &&
(!string.Equals(normalizationArgs.NormalizedIdentifier.Host, normalizationArgs.UserSuppliedIdentifier.Host, StringComparison.OrdinalIgnoreCase) ||
!string.Equals(normalizationArgs.NormalizedIdentifier.PathAndQuery, normalizationArgs.UserSuppliedIdentifier.PathAndQuery, StringComparison.Ordinal))) {
Page.Response.Redirect(normalizationArgs.NormalizedIdentifier.AbsoluteUri);
}
}
///
/// Checks the incoming request and invokes a browser redirect if the URL has not been normalized.
///
/// The object that contains the event data.
protected override void OnLoad(EventArgs e) {
// Perform URL normalization BEFORE calling base.OnLoad, to keep
// our base XrdsPublisher from over-eagerly responding with an XRDS
// document before we've redirected.
if (this.AutoNormalizeRequest && !this.Page.IsPostBack) {
this.OnNormalize();
}
base.OnLoad(e);
}
///
/// Renders OpenID identity tags.
///
/// The object that receives the server control content.
[SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Uri(Uri, string) accepts second arguments that Uri(Uri, new Uri(string)) does not that we must support.")]
protected override void Render(HtmlTextWriter writer) {
Uri requestUrlBeforeRewrites = MessagingUtilities.GetRequestUrlFromContext();
base.Render(writer);
if (!string.IsNullOrEmpty(this.ProviderEndpointUrl)) {
writer.WriteBeginTag("link");
writer.WriteAttribute("rel", this.Protocol.HtmlDiscoveryProviderKey);
writer.WriteAttribute("href", new Uri(requestUrlBeforeRewrites, this.Page.Response.ApplyAppPathModifier(this.ProviderEndpointUrl)).AbsoluteUri);
writer.Write(">");
writer.WriteEndTag("link");
writer.WriteLine();
}
if (!string.IsNullOrEmpty(this.ProviderLocalIdentifier)) {
writer.WriteBeginTag("link");
writer.WriteAttribute("rel", Protocol.HtmlDiscoveryLocalIdKey);
writer.WriteAttribute("href", new Uri(requestUrlBeforeRewrites, this.Page.Response.ApplyAppPathModifier(this.ProviderLocalIdentifier)).AbsoluteUri);
writer.Write(">");
writer.WriteEndTag("link");
writer.WriteLine();
}
}
///
/// Normalizes the URL by making the path and query lowercase, and trimming trailing slashes.
///
/// The URI to normalize.
/// The normalized URI.
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "FxCop is probably right, but we've been lowercasing host names for normalization elsewhere in the project for a long time now.")]
private static Uri BestGuessNormalization(Uri uri) {
UriBuilder uriBuilder = new UriBuilder(uri);
uriBuilder.Path = uriBuilder.Path.ToLowerInvariant();
// Ensure no trailing slash unless it is the only element of the path.
if (uriBuilder.Path != "/") {
uriBuilder.Path = uriBuilder.Path.TrimEnd('/');
}
// We trim the ? from the start of the query when we reset it because
// the UriBuilder.Query setter automatically prepends one, and we don't
// want to double them up.
uriBuilder.Query = uriBuilder.Query.TrimStart('?').ToLowerInvariant();
return uriBuilder.Uri;
}
}
}