//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
[assembly: System.Web.UI.WebResource(DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingPartyControlBase.EmbeddedJavascriptResource, "text/javascript")]
namespace DotNetOpenAuth.OpenId.RelyingParty {
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Drawing.Design;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using DotNetOpenAuth.ComponentModel;
using DotNetOpenAuth.Configuration;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId.Extensions;
using DotNetOpenAuth.OpenId.Extensions.UI;
using DotNetOpenAuth.OpenId.Messages;
using Validation;
///
/// Methods of indicating to the rest of the web site that the user has logged in.
///
[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "OnSite", Justification = "Two words intended.")]
public enum LogOnSiteNotification {
///
/// The rest of the web site is unaware that the user just completed an OpenID login.
///
None,
///
/// After the event is fired
/// the control automatically calls
/// with the as the username
/// unless the event handler sets
/// property to true.
///
FormsAuthentication,
}
///
/// How an OpenID user session should be persisted across visits.
///
public enum LogOnPersistence {
///
/// The user should only be logged in as long as the browser window remains open.
/// Nothing is persisted to help the user on a return visit. Public kiosk mode.
///
Session,
///
/// The user should only be logged in as long as the browser window remains open.
/// The OpenID Identifier is persisted to help expedite re-authentication when
/// the user visits the next time.
///
SessionAndPersistentIdentifier,
///
/// The user is issued a persistent authentication ticket so that no login is
/// necessary on their return visit.
///
PersistentAuthentication,
}
///
/// A common base class for OpenID Relying Party controls.
///
[DefaultProperty("Identifier"), ValidationProperty("Identifier")]
[ParseChildren(true), PersistChildren(false)]
public abstract class OpenIdRelyingPartyControlBase : Control, IPostBackEventHandler, IDisposable {
///
/// The manifest resource name of the javascript file to include on the hosting page.
///
internal const string EmbeddedJavascriptResource = Util.DefaultNamespace + ".OpenId.RelyingParty.OpenIdRelyingPartyControlBase.js";
///
/// The cookie used to persist the Identifier the user logged in with.
///
internal const string PersistentIdentifierCookieName = OpenIdUtilities.CustomParameterPrefix + "OpenIDIdentifier";
///
/// The callback parameter name to use to store which control initiated the auth request.
///
internal const string ReturnToReceivingControlId = OpenIdUtilities.CustomParameterPrefix + "receiver";
#region Protected internal callback parameter names
///
/// The callback parameter to use for recognizing when the callback is in a popup window or hidden iframe.
///
protected internal const string UIPopupCallbackKey = OpenIdUtilities.CustomParameterPrefix + "uipopup";
///
/// The parameter name to include in the formulated auth request so that javascript can know whether
/// the OP advertises support for the UI extension.
///
protected internal const string PopupUISupportedJSHint = OpenIdUtilities.CustomParameterPrefix + "popupUISupported";
#endregion
#region Property category constants
///
/// The "Appearance" category for properties.
///
protected const string AppearanceCategory = "Appearance";
///
/// The "Behavior" category for properties.
///
protected const string BehaviorCategory = "Behavior";
///
/// The "OpenID" category for properties and events.
///
protected const string OpenIdCategory = "OpenID";
#endregion
#region Private callback parameter names
///
/// The callback parameter for use with persisting the property.
///
private const string UsePersistentCookieCallbackKey = OpenIdUtilities.CustomParameterPrefix + "UsePersistentCookie";
///
/// The callback parameter to use for recognizing when the callback is in the parent window.
///
private const string UIPopupCallbackParentKey = OpenIdUtilities.CustomParameterPrefix + "uipopupParent";
#endregion
#region Property default values
///
/// The default value for the property.
///
private const bool StatelessDefault = false;
///
/// The default value for the property.
///
private const string ReturnToUrlDefault = "";
///
/// Default value of .
///
private const LogOnPersistence UsePersistentCookieDefault = LogOnPersistence.Session;
///
/// Default value of .
///
private const LogOnSiteNotification LogOnModeDefault = LogOnSiteNotification.FormsAuthentication;
///
/// The default value for the property.
///
private const string RealmUrlDefault = "~/";
///
/// The default value for the property.
///
private const PopupBehavior PopupDefault = PopupBehavior.Never;
///
/// The default value for the property.
///
private const bool RequireSslDefault = false;
#endregion
#region Property view state keys
///
/// The viewstate key to use for storing the value of the property.
///
private const string ExtensionsViewStateKey = "Extensions";
///
/// The viewstate key to use for the property.
///
private const string StatelessViewStateKey = "Stateless";
///
/// The viewstate key to use for the property.
///
private const string UsePersistentCookieViewStateKey = "UsePersistentCookie";
///
/// The viewstate key to use for the property.
///
private const string LogOnModeViewStateKey = "LogOnMode";
///
/// The viewstate key to use for the property.
///
private const string RealmUrlViewStateKey = "RealmUrl";
///
/// The viewstate key to use for the property.
///
private const string ReturnToUrlViewStateKey = "ReturnToUrl";
///
/// The key under which the value for the property will be stored.
///
private const string IdentifierViewStateKey = "Identifier";
///
/// The viewstate key to use for the property.
///
private const string PopupViewStateKey = "Popup";
///
/// The viewstate key to use for the property.
///
private const string RequireSslViewStateKey = "RequireSsl";
#endregion
///
/// The lifetime of the cookie used to persist the Identifier the user logged in with.
///
private static readonly TimeSpan PersistentIdentifierTimeToLiveDefault = TimeSpan.FromDays(14);
///
/// Backing field for the property.
///
private OpenIdRelyingParty relyingParty;
///
/// A value indicating whether the field contains
/// an instance that we own and should Dispose.
///
private bool relyingPartyOwned;
///
/// Initializes a new instance of the class.
///
protected OpenIdRelyingPartyControlBase() {
Reporting.RecordFeatureUse(this);
}
#region Events
///
/// Fired when the user has typed in their identifier, discovery was successful
/// and a login attempt is about to begin.
///
[Description("Fired when the user has typed in their identifier, discovery was successful and a login attempt is about to begin."), Category(OpenIdCategory)]
public event EventHandler LoggingIn;
///
/// Fired upon completion of a successful login.
///
[Description("Fired upon completion of a successful login."), Category(OpenIdCategory)]
public event EventHandler LoggedIn;
///
/// Fired when a login attempt fails.
///
[Description("Fired when a login attempt fails."), Category(OpenIdCategory)]
public event EventHandler Failed;
///
/// Fired when an authentication attempt is canceled at the OpenID Provider.
///
[Description("Fired when an authentication attempt is canceled at the OpenID Provider."), Category(OpenIdCategory)]
public event EventHandler Canceled;
///
/// Occurs when the property is changed.
///
protected event EventHandler IdentifierChanged;
#endregion
///
/// Gets or sets the instance to use.
///
/// The default value is an instance initialized according to the web.config file.
///
/// A performance optimization would be to store off the
/// instance as a static member in your web site and set it
/// to this property in your Page.Load
/// event since instantiating these instances can be expensive on
/// heavily trafficked web pages.
///
[Browsable(false)]
public virtual OpenIdRelyingParty RelyingParty {
get {
if (this.relyingParty == null) {
this.relyingParty = this.CreateRelyingParty();
this.ConfigureRelyingParty(this.relyingParty);
this.relyingPartyOwned = true;
}
return this.relyingParty;
}
set {
if (this.relyingPartyOwned && this.relyingParty != null) {
this.relyingParty.Dispose();
}
this.relyingParty = value;
this.relyingPartyOwned = false;
}
}
///
/// Gets the collection of extension requests this selector should include in generated requests.
///
[PersistenceMode(PersistenceMode.InnerProperty)]
public Collection Extensions {
get {
if (this.ViewState[ExtensionsViewStateKey] == null) {
var extensions = new Collection();
this.ViewState[ExtensionsViewStateKey] = extensions;
return extensions;
} else {
return (Collection)this.ViewState[ExtensionsViewStateKey];
}
}
}
///
/// Gets or sets a value indicating whether stateless mode is used.
///
[Bindable(true), DefaultValue(StatelessDefault), Category(OpenIdCategory)]
[Description("Controls whether stateless mode is used.")]
public bool Stateless {
get { return (bool)(ViewState[StatelessViewStateKey] ?? StatelessDefault); }
set { ViewState[StatelessViewStateKey] = value; }
}
///
/// Gets or sets the OpenID of the relying party web site.
///
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")]
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenAuth.OpenId.Realm", Justification = "Using ctor for validation.")]
[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
[Bindable(true), DefaultValue(RealmUrlDefault), Category(OpenIdCategory)]
[Description("The OpenID Realm of the relying party web site.")]
[UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string RealmUrl {
get {
return (string)(ViewState[RealmUrlViewStateKey] ?? RealmUrlDefault);
}
set {
Requires.NotNullOrEmpty(value, "value");
if (Page != null && !DesignMode) {
// Validate new value by trying to construct a Realm object based on it.
new Realm(OpenIdUtilities.GetResolvedRealm(this.Page, value, new HttpRequestWrapper(this.Context.Request))); // 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.Replace("*.", string.Empty)); // make sure it's fully-qualified, but ignore wildcards
} else if (value.StartsWith("~/", StringComparison.Ordinal)) {
// this is valid too
} else {
throw new UriFormatException();
}
}
ViewState[RealmUrlViewStateKey] = value;
}
}
///
/// Gets or sets the OpenID ReturnTo of the relying party web site.
///
[SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Bindable property must be simple type")]
[SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri", Justification = "Using Uri.ctor for validation.")]
[SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Bindable property must be simple type")]
[Bindable(true), DefaultValue(ReturnToUrlDefault), Category(OpenIdCategory)]
[Description("The OpenID ReturnTo of the relying party web site.")]
[UrlProperty, Editor("System.Web.UI.Design.UrlEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string ReturnToUrl {
get {
return (string)(this.ViewState[ReturnToUrlViewStateKey] ?? ReturnToUrlDefault);
}
set {
if (this.Page != null && !this.DesignMode) {
// Validate new value by trying to construct a Uri based on it.
new Uri(new HttpRequestWrapper(this.Context.Request).GetPublicFacingUrl(), 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[ReturnToUrlViewStateKey] = value;
}
}
///
/// Gets or sets a value indicating whether to send a persistent cookie upon successful
/// login so the user does not have to log in upon returning to this site.
///
[Bindable(true), DefaultValue(UsePersistentCookieDefault), Category(BehaviorCategory)]
[Description("Whether to send a persistent cookie upon successful " +
"login so the user does not have to log in upon returning to this site.")]
public virtual LogOnPersistence UsePersistentCookie {
get { return (LogOnPersistence)(this.ViewState[UsePersistentCookieViewStateKey] ?? UsePersistentCookieDefault); }
set { this.ViewState[UsePersistentCookieViewStateKey] = value; }
}
///
/// Gets or sets the way a completed login is communicated to the rest of the web site.
///
[Bindable(true), DefaultValue(LogOnModeDefault), Category(BehaviorCategory)]
[Description("The way a completed login is communicated to the rest of the web site.")]
public virtual LogOnSiteNotification LogOnMode {
get { return (LogOnSiteNotification)(this.ViewState[LogOnModeViewStateKey] ?? LogOnModeDefault); }
set { this.ViewState[LogOnModeViewStateKey] = value; }
}
///
/// Gets or sets a value indicating when to use a popup window to complete the login experience.
///
/// The default value is .
[Bindable(true), DefaultValue(PopupDefault), Category(BehaviorCategory)]
[Description("When to use a popup window to complete the login experience.")]
public virtual PopupBehavior Popup {
get { return (PopupBehavior)(ViewState[PopupViewStateKey] ?? PopupDefault); }
set { ViewState[PopupViewStateKey] = value; }
}
///
/// Gets or sets a value indicating whether to enforce on high security mode,
/// which requires the full authentication pipeline to be protected by SSL.
///
[Bindable(true), DefaultValue(RequireSslDefault), Category(OpenIdCategory)]
[Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")]
public bool RequireSsl {
get { return (bool)(ViewState[RequireSslViewStateKey] ?? RequireSslDefault); }
set { ViewState[RequireSslViewStateKey] = value; }
}
///
/// Gets or sets the Identifier that will be used to initiate login.
///
[Bindable(true), Category(OpenIdCategory)]
[Description("The OpenID Identifier that this button will use to initiate login.")]
[TypeConverter(typeof(IdentifierConverter))]
public virtual Identifier Identifier {
get {
return (Identifier)ViewState[IdentifierViewStateKey];
}
set {
ViewState[IdentifierViewStateKey] = value;
this.OnIdentifierChanged();
}
}
///
/// Gets or sets the default association preference to set on authentication requests.
///
internal AssociationPreference AssociationPreference { get; set; }
///
/// Gets ancestor controls, starting with the immediate parent, and progressing to more distant ancestors.
///
protected IEnumerable ParentControls {
get {
Control parent = this;
while ((parent = parent.Parent) != null) {
yield return parent;
}
}
}
///
/// Gets a value indicating whether this control is a child control of a composite OpenID control.
///
///
/// true if this instance is embedded in parent OpenID control; otherwise, false.
///
protected bool IsEmbeddedInParentOpenIdControl {
get { return this.ParentControls.OfType().Any(); }
}
///
/// Clears any cookie set by this control to help the user on a returning visit next time.
///
public static void LogOff() {
HttpContext.Current.Response.SetCookie(CreateIdentifierPersistingCookie(null));
}
///
/// Immediately redirects to the OpenID Provider to verify the Identifier
/// provided in the text box.
///
public async Task LogOnAsync(CancellationToken cancellationToken) {
IAuthenticationRequest request = (await this.CreateRequestsAsync(cancellationToken)).FirstOrDefault();
ErrorUtilities.VerifyProtocol(request != null, OpenIdStrings.OpenIdEndpointNotFound);
await this.LogOnAsync(request, cancellationToken);
}
///
/// Immediately redirects to the OpenID Provider to verify the Identifier
/// provided in the text box.
///
/// The request.
public async Task LogOnAsync(IAuthenticationRequest request, CancellationToken cancellationToken) {
Requires.NotNull(request, "request");
if (this.IsPopupAppropriate(request)) {
await this.ScriptPopupWindowAsync(request, cancellationToken);
} else {
await request.RedirectToProviderAsync(cancellationToken: cancellationToken);
}
}
#region IPostBackEventHandler Members
///
/// 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);
}
#endregion
///
/// Enables a server control to perform final clean up before it is released from memory.
///
[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Unavoidable because base class does not expose a protected virtual Dispose(bool) method."), SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "Base class doesn't implement virtual Dispose(bool), so we must call its Dispose() method.")]
public sealed override void Dispose() {
this.Dispose(true);
base.Dispose();
GC.SuppressFinalize(this);
}
///
/// Creates the authentication requests for a given user-supplied Identifier.
///
/// The identifier to create a request for.
///
/// A sequence of authentication requests, any one of which may be
/// used to determine the user's control of the .
///
protected internal virtual Task> CreateRequestsAsync(Identifier identifier, CancellationToken cancellationToken) {
Requires.NotNull(identifier, "identifier");
// If this control is actually a member of another OpenID RP control,
// delegate creation of requests to the parent control.
var parentOwner = this.ParentControls.OfType().FirstOrDefault();
if (parentOwner != null) {
return parentOwner.CreateRequestsAsync(identifier, cancellationToken);
} else {
// Delegate to a private method to keep 'yield return' and Code Contract separate.
return this.CreateRequestsCoreAsync(identifier, cancellationToken);
}
}
///
/// Releases unmanaged and - optionally - managed resources
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing) {
if (disposing) {
if (this.relyingPartyOwned && this.relyingParty != null) {
this.relyingParty.Dispose();
this.relyingParty = null;
}
}
}
///
/// 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) {
}
///
/// Creates the authentication requests for the value set in the property.
///
///
/// A sequence of authentication requests, any one of which may be
/// used to determine the user's control of the .
///
protected Task> CreateRequestsAsync(CancellationToken cancellationToken) {
RequiresEx.ValidState(this.Identifier != null, OpenIdStrings.NoIdentifierSet);
return this.CreateRequestsAsync(this.Identifier, cancellationToken);
}
///
/// Raises the event.
///
/// The instance containing the event data.
protected override void OnLoad(EventArgs e) {
base.OnLoad(e);
if (Page.IsPostBack) {
// OpenID responses NEVER come in the form of a postback.
return;
}
if (this.Identifier == null) {
this.TryPresetIdentifierWithCookie();
}
// Take an unreliable sneek peek to see if we're in a popup and an OpenID
// assertion is coming in. We shouldn't process assertions in a popup window.
if (this.Page.Request.QueryString[UIPopupCallbackKey] == "1"
&& this.Page.Request.QueryString[UIPopupCallbackParentKey] == null) {
// We're in a popup window. We need to close it and pass the
// message back to the parent window for processing.
this.ScriptClosingPopupOrIFrameAsync(CancellationToken.None).Wait();
return; // don't do any more processing on it now
}
// Only sniff for an OpenID response if it is targeted at this control.
// Note that Stateless mode causes no receiver to be indicated, and
// we want to handle that, but only if there isn't a parent control that
// will be handling that.
string receiver = this.Page.Request.QueryString[ReturnToReceivingControlId]
?? this.Page.Request.Form[ReturnToReceivingControlId];
if (receiver == this.ClientID || (receiver == null && !this.IsEmbeddedInParentOpenIdControl)) {
var response = Task.Run(() => this.RelyingParty.GetResponseAsync(new HttpRequestWrapper(this.Context.Request), CancellationToken.None)).GetAwaiter().GetResult();
Logger.Controls.DebugFormat(
"The {0} control checked for an authentication response and found: {1}",
this.ID,
response != null ? response.Status.ToString() : "nothing");
this.ProcessResponse(response);
}
}
///
/// Notifies the user agent via an AJAX response of a completed authentication attempt.
///
protected virtual Task ScriptClosingPopupOrIFrameAsync(CancellationToken cancellationToken) {
return this.RelyingParty.ProcessResponseFromPopupAsync(cancellationToken);
}
///
/// Called when the property is changed.
///
protected virtual void OnIdentifierChanged() {
var identifierChanged = this.IdentifierChanged;
if (identifierChanged != null) {
identifierChanged(this, EventArgs.Empty);
}
}
///
/// Processes the response.
///
/// The response.
protected virtual void ProcessResponse(IAuthenticationResponse response) {
if (response == null) {
return;
}
string persistentString = response.GetUntrustedCallbackArgument(UsePersistentCookieCallbackKey);
if (persistentString != null) {
this.UsePersistentCookie = (LogOnPersistence)Enum.Parse(typeof(LogOnPersistence), persistentString);
}
switch (response.Status) {
case AuthenticationStatus.Authenticated:
this.OnLoggedIn(response);
break;
case AuthenticationStatus.Canceled:
this.OnCanceled(response);
break;
case AuthenticationStatus.Failed:
this.OnFailed(response);
break;
case AuthenticationStatus.SetupRequired:
case AuthenticationStatus.ExtensionsOnly:
default:
// The NotApplicable (extension-only assertion) is NOT one that we support
// in this control because that scenario is primarily interesting to RPs
// that are asking a specific OP, and it is not user-initiated as this textbox
// is designed for.
throw new InvalidOperationException(MessagingStrings.UnexpectedMessageReceivedOfMany);
}
}
///
/// Raises the event.
///
/// An object that contains the event data.
protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
this.Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdRelyingPartyControlBase), EmbeddedJavascriptResource);
}
///
/// Fires the event.
///
/// The response.
protected virtual void OnLoggedIn(IAuthenticationResponse response) {
Requires.NotNull(response, "response");
Requires.That(response.Status == AuthenticationStatus.Authenticated, "response", "response not authenticatedl");
var loggedIn = this.LoggedIn;
OpenIdEventArgs args = new OpenIdEventArgs(response);
if (loggedIn != null) {
loggedIn(this, args);
}
if (!args.Cancel) {
if (this.UsePersistentCookie == LogOnPersistence.SessionAndPersistentIdentifier) {
Page.Response.SetCookie(CreateIdentifierPersistingCookie(response));
}
switch (this.LogOnMode) {
case LogOnSiteNotification.FormsAuthentication:
FormsAuthentication.RedirectFromLoginPage(response.ClaimedIdentifier, this.UsePersistentCookie == LogOnPersistence.PersistentAuthentication);
break;
case LogOnSiteNotification.None:
default:
break;
}
}
}
///
/// Fires the event.
///
/// The request.
///
/// Returns whether the login should proceed. False if some event handler canceled the request.
///
protected virtual bool OnLoggingIn(IAuthenticationRequest request) {
Requires.NotNull(request, "request");
EventHandler loggingIn = this.LoggingIn;
OpenIdEventArgs args = new OpenIdEventArgs(request);
if (loggingIn != null) {
loggingIn(this, args);
}
return !args.Cancel;
}
///
/// Fires the event.
///
/// The response.
protected virtual void OnCanceled(IAuthenticationResponse response) {
Requires.NotNull(response, "response");
Requires.That(response.Status == AuthenticationStatus.Canceled, "response", "response not canceled");
var canceled = this.Canceled;
if (canceled != null) {
canceled(this, new OpenIdEventArgs(response));
}
}
///
/// Fires the event.
///
/// The response.
protected virtual void OnFailed(IAuthenticationResponse response) {
Requires.NotNull(response, "response");
Requires.That(response.Status == AuthenticationStatus.Failed, "response", "response not failed");
var failed = this.Failed;
if (failed != null) {
failed(this, new OpenIdEventArgs(response));
}
}
///
/// Creates the relying party instance used to generate authentication requests.
///
/// The instantiated relying party.
protected OpenIdRelyingParty CreateRelyingParty() {
IOpenIdApplicationStore store = this.Stateless ? null : OpenIdElement.Configuration.RelyingParty.ApplicationStore.CreateInstance(OpenIdRelyingParty.GetHttpApplicationStore(new HttpContextWrapper(this.Context)), null);
return this.CreateRelyingParty(store);
}
///
/// Creates the relying party instance used to generate authentication requests.
///
/// The store to pass to the relying party constructor.
/// The instantiated relying party.
protected virtual OpenIdRelyingParty CreateRelyingParty(IOpenIdApplicationStore store) {
return new OpenIdRelyingParty(store);
}
///
/// Configures the relying party.
///
/// The relying party.
[SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "relyingParty", Justification = "This makes it possible for overrides to see the value before it is set on a field.")]
protected virtual void ConfigureRelyingParty(OpenIdRelyingParty relyingParty) {
Requires.NotNull(relyingParty, "relyingParty");
// Only set RequireSsl to true, as we don't want to override
// a .config setting of true with false.
if (this.RequireSsl) {
relyingParty.SecuritySettings.RequireSsl = true;
}
}
///
/// Detects whether a popup window should be used to show the Provider's UI.
///
/// The request.
///
/// true if a popup should be used; false otherwise.
///
protected virtual bool IsPopupAppropriate(IAuthenticationRequest request) {
Requires.NotNull(request, "request");
switch (this.Popup) {
case PopupBehavior.Never:
return false;
case PopupBehavior.Always:
return true;
case PopupBehavior.IfProviderSupported:
return request.DiscoveryResult.IsExtensionSupported();
default:
throw ErrorUtilities.ThrowInternal("Unexpected value for Popup property.");
}
}
///
/// Adds attributes to an HTML <A> tag that will be written by the caller using
/// after this method.
///
/// The HTML writer.
/// The outgoing authentication request.
/// The text to try to display in the status bar on mouse hover.
protected async Task RenderOpenIdMessageTransmissionAsAnchorAttributesAsync(HtmlTextWriter writer, IAuthenticationRequest request, string windowStatus, CancellationToken cancellationToken) {
Requires.NotNull(writer, "writer");
Requires.NotNull(request, "request");
// We render a standard HREF attribute for non-javascript browsers.
var response = await request.GetRedirectingResponseAsync(cancellationToken);
writer.AddAttribute(HtmlTextWriterAttribute.Href, response.GetDirectUriRequest().AbsoluteUri);
// And for the Javascript ones we do the extra work to use form POST where necessary.
writer.AddAttribute(HtmlTextWriterAttribute.Onclick, await this.CreateGetOrPostAHrefValueAsync(request, cancellationToken) + " return false;");
writer.AddStyleAttribute(HtmlTextWriterStyle.Cursor, "pointer");
if (!string.IsNullOrEmpty(windowStatus)) {
writer.AddAttribute("onMouseOver", "window.status = " + MessagingUtilities.GetSafeJavascriptValue(windowStatus));
writer.AddAttribute("onMouseOut", "window.status = null");
}
}
///
/// Creates the identifier-persisting cookie, either for saving or deleting.
///
/// The positive authentication response; or null to clear the cookie.
/// An persistent cookie.
private static HttpCookie CreateIdentifierPersistingCookie(IAuthenticationResponse response) {
HttpCookie cookie = new HttpCookie(PersistentIdentifierCookieName);
bool clearingCookie = false;
// We'll try to store whatever it was the user originally typed in, but fallback
// to the final claimed_id.
if (response != null && response.Status == AuthenticationStatus.Authenticated) {
var positiveResponse = (PositiveAuthenticationResponse)response;
// We must escape the value because XRIs start with =, and any leading '=' gets dropped (by ASP.NET?)
cookie.Value = Uri.EscapeDataString(positiveResponse.Endpoint.UserSuppliedIdentifier ?? response.ClaimedIdentifier);
} else {
clearingCookie = true;
cookie.Value = string.Empty;
if (HttpContext.Current.Request.Browser["supportsEmptyStringInCookieValue"] == "false") {
cookie.Value = "NoCookie";
}
}
if (clearingCookie) {
// mark the cookie has having already expired to cause the user agent to delete
// the old persisted cookie.
cookie.Expires = DateTime.Now.Subtract(TimeSpan.FromDays(1));
} else {
// Make the cookie persistent by setting an expiration date
cookie.Expires = DateTime.Now + PersistentIdentifierTimeToLiveDefault;
}
return cookie;
}
///
/// Creates the authentication requests for a given user-supplied Identifier.
///
/// The identifier to create a request for.
///
/// A sequence of authentication requests, any one of which may be
/// used to determine the user's control of the .
///
private async Task> CreateRequestsCoreAsync(Identifier identifier, CancellationToken cancellationToken) {
ErrorUtilities.VerifyArgumentNotNull(identifier, "identifier"); // NO CODE CONTRACTS! (yield return used here)
IEnumerable requests;
var requestContext = new HttpRequestWrapper(this.Context.Request);
var results = new List();
// Approximate the returnTo (either based on the customize property or the page URL)
// so we can use it to help with Realm resolution.
Uri returnToApproximation;
if (this.ReturnToUrl != null) {
string returnToResolvedPath = this.ResolveUrl(this.ReturnToUrl);
returnToApproximation = new Uri(requestContext.GetPublicFacingUrl(), returnToResolvedPath);
} else {
returnToApproximation = this.Page.Request.Url;
}
// Resolve the trust root, and swap out the scheme and port if necessary to match the
// return_to URL, since this match is required by OpenID, and the consumer app
// may be using HTTP at some times and HTTPS at others.
UriBuilder realm = OpenIdUtilities.GetResolvedRealm(this.Page, this.RealmUrl, requestContext);
realm.Scheme = returnToApproximation.Scheme;
realm.Port = returnToApproximation.Port;
// Initiate OpenID request
// We use TryParse here to avoid throwing an exception which
// might slip through our validator control if it is disabled.
Realm typedRealm = new Realm(realm);
if (string.IsNullOrEmpty(this.ReturnToUrl)) {
requests = await this.RelyingParty.CreateRequestsAsync(identifier, typedRealm, requestContext);
} else {
// Since the user actually gave us a return_to value,
// the "approximation" is exactly what we want.
requests = await this.RelyingParty.CreateRequestsAsync(identifier, typedRealm, returnToApproximation, cancellationToken);
}
// Some OPs may be listed multiple times (one with HTTPS and the other with HTTP, for example).
// Since we're gathering OPs to try one after the other, just take the first choice of each OP
// and don't try it multiple times.
requests = requests.Distinct(DuplicateRequestedHostsComparer.Instance);
// Configure each generated request.
foreach (var req in requests) {
if (this.IsPopupAppropriate(req)) {
// Inform ourselves in return_to that we're in a popup.
req.SetUntrustedCallbackArgument(UIPopupCallbackKey, "1");
if (req.DiscoveryResult.IsExtensionSupported()) {
// Inform the OP that we'll be using a popup window consistent with the UI extension.
// But beware that the extension MAY have already been added if we're using
// the OpenIdAjaxRelyingParty class.
if (!((AuthenticationRequest)req).Extensions.OfType().Any()) {
req.AddExtension(new UIRequest());
}
// Provide a hint for the client javascript about whether the OP supports the UI extension.
// This is so the window can be made the correct size for the extension.
// If the OP doesn't advertise support for the extension, the javascript will use
// a bigger popup window.
req.SetUntrustedCallbackArgument(PopupUISupportedJSHint, "1");
}
}
// Add the extensions injected into the control.
foreach (var extension in this.Extensions) {
req.AddExtension(extension);
}
// Add state that needs to survive across the redirect, but at this point
// only save those properties that are not expected to be changed by a
// LoggingIn event handler.
req.SetUntrustedCallbackArgument(ReturnToReceivingControlId, this.ClientID);
// Apply the control's association preference to this auth request, but only if
// it is less demanding (greater ordinal value) than the existing one.
// That way, we protect against retrying an association that was already attempted.
var authReq = (AuthenticationRequest)req;
if (authReq.AssociationPreference < this.AssociationPreference) {
authReq.AssociationPreference = this.AssociationPreference;
}
if (this.OnLoggingIn(req)) {
// We save this property after firing OnLoggingIn so that the host page can
// change its value and have that value saved.
req.SetUntrustedCallbackArgument(UsePersistentCookieCallbackKey, this.UsePersistentCookie.ToString());
results.Add(req);
}
}
return results;
}
///
/// Gets the javascript to executee to redirect or POST an OpenID message to a remote party.
///
/// The authentication request to send.
/// The javascript that should execute.
private async Task CreateGetOrPostAHrefValueAsync(IAuthenticationRequest request, CancellationToken cancellationToken) {
Requires.NotNull(request, "request");
var response = await request.GetRedirectingResponseAsync(cancellationToken);
Uri directUri = response.GetDirectUriRequest();
return "window.dnoa_internal.GetOrPost(" + MessagingUtilities.GetSafeJavascriptValue(directUri.AbsoluteUri) + ");";
}
///
/// Wires the return page to immediately display a popup window with the Provider in it.
///
/// The request.
private async Task ScriptPopupWindowAsync(IAuthenticationRequest request, CancellationToken cancellationToken) {
Requires.NotNull(request, "request");
RequiresEx.ValidState(this.RelyingParty != null);
StringBuilder startupScript = new StringBuilder();
// Add a callback function that the popup window can call on this, the
// parent window, to pass back the authentication result.
startupScript.AppendLine("window.dnoa_internal = {};");
startupScript.AppendLine("window.dnoa_internal.processAuthorizationResult = function(uri) { window.location = uri; };");
startupScript.AppendLine("window.dnoa_internal.popupWindow = function() {");
startupScript.AppendFormat(
@"\tvar openidPopup = {0}",
await OpenId.RelyingParty.Extensions.UI.UIUtilities.GetWindowPopupScriptAsync(this.RelyingParty, request, "openidPopup", cancellationToken));
startupScript.AppendLine("};");
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "loginPopup", startupScript.ToString(), true);
}
///
/// Tries to preset the property based on a persistent
/// cookie on the browser.
///
///
/// A value indicating whether the property was
/// successfully preset to some non-empty value.
///
private bool TryPresetIdentifierWithCookie() {
HttpCookie cookie = this.Page.Request.Cookies[PersistentIdentifierCookieName];
if (cookie != null) {
this.Identifier = Uri.UnescapeDataString(cookie.Value);
return true;
}
return false;
}
}
}