summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs')
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs891
1 files changed, 891 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs
new file mode 100644
index 0000000..aec59b8
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -0,0 +1,891 @@
+//-----------------------------------------------------------------------
+// <copyright file="OpenIdRelyingParty.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.ObjectModel;
+ using System.Collections.Specialized;
+ using System.ComponentModel;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Globalization;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Configuration;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Bindings;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Extensions;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// A delegate that decides whether a given OpenID Provider endpoint may be
+ /// considered for authenticating a user.
+ /// </summary>
+ /// <param name="endpoint">The endpoint for consideration.</param>
+ /// <returns>
+ /// <c>True</c> if the endpoint should be considered.
+ /// <c>False</c> to remove it from the pool of acceptable providers.
+ /// </returns>
+ public delegate bool EndpointSelector(IProviderEndpoint endpoint);
+
+ /// <summary>
+ /// Provides the programmatic facilities to act as an OpenID relying party.
+ /// </summary>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")]
+ [ContractVerification(true)]
+ public class OpenIdRelyingParty : IDisposable {
+ /// <summary>
+ /// The name of the key to use in the HttpApplication cache to store the
+ /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use.
+ /// </summary>
+ private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.HttpApplicationStore";
+
+ /// <summary>
+ /// Backing store for the <see cref="Behaviors"/> property.
+ /// </summary>
+ private readonly ObservableCollection<IRelyingPartyBehavior> behaviors = new ObservableCollection<IRelyingPartyBehavior>();
+
+ /// <summary>
+ /// Backing field for the <see cref="DiscoveryServices"/> property.
+ /// </summary>
+ private readonly IList<IIdentifierDiscoveryService> discoveryServices = new List<IIdentifierDiscoveryService>(2);
+
+ /// <summary>
+ /// Backing field for the <see cref="NonVerifyingRelyingParty"/> property.
+ /// </summary>
+ private OpenIdRelyingParty nonVerifyingRelyingParty;
+
+ /// <summary>
+ /// The lock to obtain when initializing the <see cref="nonVerifyingRelyingParty"/> member.
+ /// </summary>
+ private object nonVerifyingRelyingPartyInitLock = new object();
+
+ /// <summary>
+ /// A dictionary of extension response types and the javascript member
+ /// name to map them to on the user agent.
+ /// </summary>
+ private Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>();
+
+ /// <summary>
+ /// Backing field for the <see cref="SecuritySettings"/> property.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Backing store for the <see cref="EndpointOrder"/> property.
+ /// </summary>
+ private Comparison<IdentifierDiscoveryResult> endpointOrder = DefaultEndpointOrder;
+
+ /// <summary>
+ /// Backing field for the <see cref="Channel"/> property.
+ /// </summary>
+ private Channel channel;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ public OpenIdRelyingParty()
+ : this(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ /// <param name="applicationStore">The application store. If <c>null</c>, the relying party will always operate in "dumb mode".</param>
+ public OpenIdRelyingParty(IOpenIdApplicationStore applicationStore)
+ : this(applicationStore, applicationStore) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
+ /// </summary>
+ /// <param name="cryptoKeyStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
+ /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
+ [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")]
+ private OpenIdRelyingParty(ICryptoKeyStore cryptoKeyStore, INonceStore nonceStore) {
+ // If we are a smart-mode RP (supporting associations), then we MUST also be
+ // capable of storing nonces to prevent replay attacks.
+ // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
+ Contract.Requires<ArgumentException>(cryptoKeyStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
+
+ this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings();
+
+ foreach (var discoveryService in DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.DiscoveryServices.CreateInstances(true)) {
+ this.discoveryServices.Add(discoveryService);
+ }
+
+ this.behaviors.CollectionChanged += this.OnBehaviorsChanged;
+ foreach (var behavior in DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.Behaviors.CreateInstances(false)) {
+ this.behaviors.Add(behavior);
+ }
+
+ // Without a nonce store, we must rely on the Provider to protect against
+ // replay attacks. But only 2.0+ Providers can be expected to provide
+ // replay protection.
+ if (nonceStore == null &&
+ this.SecuritySettings.ProtectDownlevelReplayAttacks &&
+ this.SecuritySettings.MinimumRequiredOpenIdVersion < ProtocolVersion.V20) {
+ Logger.OpenId.Warn("Raising minimum OpenID version requirement for Providers to 2.0 to protect this stateless RP from replay attacks.");
+ this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
+ }
+
+ if (cryptoKeyStore == null) {
+ cryptoKeyStore = new MemoryCryptoKeyStore();
+ }
+
+ this.channel = new OpenIdChannel(cryptoKeyStore, nonceStore, this.SecuritySettings);
+ this.AssociationManager = new AssociationManager(this.Channel, new CryptoKeyStoreAsRelyingPartyAssociationStore(cryptoKeyStore), this.SecuritySettings);
+
+ Reporting.RecordFeatureAndDependencyUse(this, cryptoKeyStore, nonceStore);
+ }
+
+ /// <summary>
+ /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
+ /// attribute to determine order.
+ /// </summary>
+ /// <remarks>
+ /// Endpoints lacking any priority value are sorted to the end of the list.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static Comparison<IdentifierDiscoveryResult> DefaultEndpointOrder {
+ get { return IdentifierDiscoveryResult.EndpointOrder; }
+ }
+
+ /// <summary>
+ /// Gets the standard state storage mechanism that uses ASP.NET's
+ /// HttpApplication state dictionary to store associations and nonces.
+ /// </summary>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static IOpenIdApplicationStore HttpApplicationStore {
+ get {
+ Contract.Ensures(Contract.Result<IOpenIdApplicationStore>() != null);
+
+ HttpContext context = HttpContext.Current;
+ ErrorUtilities.VerifyOperation(context != null, Strings.StoreRequiredWhenNoHttpContextAvailable, typeof(IOpenIdApplicationStore).Name);
+ var store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey];
+ if (store == null) {
+ context.Application.Lock();
+ try {
+ if ((store = (IOpenIdApplicationStore)context.Application[ApplicationStoreKey]) == null) {
+ context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore();
+ }
+ } finally {
+ context.Application.UnLock();
+ }
+ }
+
+ return store;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the channel to use for sending/receiving messages.
+ /// </summary>
+ public Channel Channel {
+ get {
+ return this.channel;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.channel = value;
+ this.AssociationManager.Channel = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the security settings used by this Relying Party.
+ /// </summary>
+ public RelyingPartySecuritySettings SecuritySettings {
+ get {
+ Contract.Ensures(Contract.Result<RelyingPartySecuritySettings>() != null);
+ return this.securitySettings;
+ }
+
+ internal set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.securitySettings = value;
+ this.AssociationManager.SecuritySettings = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the optional Provider Endpoint filter to use.
+ /// </summary>
+ /// <remarks>
+ /// Provides a way to optionally filter the providers that may be used in authenticating a user.
+ /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
+ /// If null, all identity providers will be accepted. This is the default.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public EndpointSelector EndpointFilter { get; set; }
+
+ /// <summary>
+ /// Gets or sets the ordering routine that will determine which XRDS
+ /// Service element to try first
+ /// </summary>
+ /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
+ /// <remarks>
+ /// This may never be null. To reset to default behavior this property
+ /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public Comparison<IdentifierDiscoveryResult> EndpointOrder {
+ get {
+ return this.endpointOrder;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.endpointOrder = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets the extension factories.
+ /// </summary>
+ public IList<IOpenIdExtensionFactory> ExtensionFactories {
+ get { return this.Channel.GetExtensionFactories(); }
+ }
+
+ /// <summary>
+ /// Gets a list of custom behaviors to apply to OpenID actions.
+ /// </summary>
+ /// <remarks>
+ /// Adding behaviors can impact the security settings of this <see cref="OpenIdRelyingParty"/>
+ /// instance in ways that subsequently removing the behaviors will not reverse.
+ /// </remarks>
+ public ICollection<IRelyingPartyBehavior> Behaviors {
+ get { return this.behaviors; }
+ }
+
+ /// <summary>
+ /// Gets the list of services that can perform discovery on identifiers given to this relying party.
+ /// </summary>
+ public IList<IIdentifierDiscoveryService> DiscoveryServices {
+ get { return this.discoveryServices; }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this Relying Party can sign its return_to
+ /// parameter in outgoing authentication requests.
+ /// </summary>
+ internal bool CanSignCallbackArguments {
+ get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); }
+ }
+
+ /// <summary>
+ /// Gets the web request handler to use for discovery and the part of
+ /// authentication where direct messages are sent to an untrusted remote party.
+ /// </summary>
+ internal IDirectWebRequestHandler WebRequestHandler {
+ get { return this.Channel.WebRequestHandler; }
+ }
+
+ /// <summary>
+ /// Gets the association manager.
+ /// </summary>
+ internal AssociationManager AssociationManager { get; private set; }
+
+ /// <summary>
+ /// Gets the <see cref="OpenIdRelyingParty"/> instance used to process authentication responses
+ /// without verifying the assertion or consuming nonces.
+ /// </summary>
+ protected OpenIdRelyingParty NonVerifyingRelyingParty {
+ get {
+ if (this.nonVerifyingRelyingParty == null) {
+ lock (this.nonVerifyingRelyingPartyInitLock) {
+ if (this.nonVerifyingRelyingParty == null) {
+ this.nonVerifyingRelyingParty = OpenIdRelyingParty.CreateNonVerifying();
+ }
+ }
+ }
+
+ return this.nonVerifyingRelyingParty;
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// An authentication request object to customize the request and generate
+ /// an object to send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Requires<ArgumentNullException>(returnToUrl != null);
+ Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null);
+ try {
+ return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null);
+ try {
+ var result = this.CreateRequests(userSuppliedIdentifier, realm).First();
+ Contract.Assume(result != null);
+ return result;
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Ensures(Contract.Result<IAuthenticationRequest>() != null);
+ try {
+ return this.CreateRequests(userSuppliedIdentifier).First();
+ } catch (InvalidOperationException ex) {
+ throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
+ }
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <param name="returnToUrl">
+ /// The URL of the login page, or the page prepared to receive authentication
+ /// responses from the OpenID Provider.
+ /// </param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// </remarks>
+ public virtual IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Requires<ArgumentNullException>(returnToUrl != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null);
+
+ return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>().CacheGeneratedResults();
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<ArgumentNullException>(realm != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null);
+
+ // This next code contract is a BAD idea, because it causes each authentication request to be generated
+ // at least an extra time.
+ ////Contract.Ensures(Contract.ForAll(Contract.Result<IEnumerable<IAuthenticationRequest>>(), el => el != null));
+
+ // Build the return_to URL
+ UriBuilder returnTo = new UriBuilder(this.Channel.GetRequestFromContext().UrlBeforeRewriting);
+
+ // Trim off any parameters with an "openid." prefix, and a few known others
+ // to avoid carrying state from a prior login attempt.
+ returnTo.Query = string.Empty;
+ NameValueCollection queryParams = this.Channel.GetRequestFromContext().QueryStringBeforeRewriting;
+ var returnToParams = new Dictionary<string, string>(queryParams.Count);
+ foreach (string key in queryParams) {
+ if (!IsOpenIdSupportingParameter(key) && key != null) {
+ returnToParams.Add(key, queryParams[key]);
+ }
+ }
+ returnTo.AppendQueryArgs(returnToParams);
+
+ return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
+ }
+
+ /// <summary>
+ /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
+ /// Never null, but may be empty.
+ /// </returns>
+ /// <remarks>
+ /// <para>Any individual generated request can satisfy the authentication.
+ /// The generated requests are sorted in preferred order.
+ /// Each request is generated as it is enumerated to. Associations are created only as
+ /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
+ /// <para>No exception is thrown if no OpenID endpoints were discovered.
+ /// An empty enumerable is returned instead.</para>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
+ public IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
+ Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null);
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Ensures(Contract.Result<IEnumerable<IAuthenticationRequest>>() != null);
+
+ return this.CreateRequests(userSuppliedIdentifier, Realm.AutoDetect);
+ }
+
+ /// <summary>
+ /// Gets an authentication response from a Provider.
+ /// </summary>
+ /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ public IAuthenticationResponse GetResponse() {
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ return this.GetResponse(this.Channel.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Gets an authentication response from a Provider.
+ /// </summary>
+ /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
+ /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
+ public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
+ Contract.Requires<ArgumentNullException>(httpRequestInfo != null);
+ try {
+ var message = this.Channel.ReadFromRequest(httpRequestInfo);
+ PositiveAssertionResponse positiveAssertion;
+ NegativeAssertionResponse negativeAssertion;
+ IndirectSignedResponse positiveExtensionOnly;
+ if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
+ // We need to make sure that this assertion is coming from an endpoint
+ // that the host deems acceptable.
+ var providerEndpoint = new SimpleXrdsProviderEndpoint(positiveAssertion);
+ ErrorUtilities.VerifyProtocol(
+ this.FilterEndpoint(providerEndpoint),
+ OpenIdStrings.PositiveAssertionFromNonQualifiedProvider,
+ providerEndpoint.Uri);
+
+ var response = new PositiveAuthenticationResponse(positiveAssertion, this);
+ foreach (var behavior in this.Behaviors) {
+ behavior.OnIncomingPositiveAssertion(response);
+ }
+
+ return response;
+ } else if ((positiveExtensionOnly = message as IndirectSignedResponse) != null) {
+ return new PositiveAnonymousResponse(positiveExtensionOnly);
+ } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
+ return new NegativeAuthenticationResponse(negativeAssertion);
+ } else if (message != null) {
+ Logger.OpenId.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
+ }
+
+ return null;
+ } catch (ProtocolException ex) {
+ return new FailedAuthenticationResponse(ex);
+ }
+ }
+
+ /// <summary>
+ /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication.
+ /// </summary>
+ /// <returns>The HTTP response to send to this HTTP request.</returns>
+ /// <remarks>
+ /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
+ /// </remarks>
+ public OutgoingWebResponse ProcessResponseFromPopup() {
+ Contract.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+
+ return this.ProcessResponseFromPopup(this.Channel.GetRequestFromContext());
+ }
+
+ /// <summary>
+ /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication.
+ /// </summary>
+ /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param>
+ /// <returns>The HTTP response to send to this HTTP request.</returns>
+ public OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+
+ return this.ProcessResponseFromPopup(request, null);
+ }
+
+ /// <summary>
+ /// Allows an OpenID extension to read data out of an unverified positive authentication assertion
+ /// and send it down to the client browser so that Javascript running on the page can perform
+ /// some preprocessing on the extension data.
+ /// </summary>
+ /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam>
+ /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param>
+ /// <remarks>
+ /// This method should be called before <see cref="ProcessResponseFromPopup()"/>.
+ /// </remarks>
+ [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "By design")]
+ public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(propertyName));
+ ErrorUtilities.VerifyArgumentNamed(!this.clientScriptExtensions.ContainsValue(propertyName), "propertyName", OpenIdStrings.ClientScriptExtensionPropertyNameCollision, propertyName);
+ foreach (var ext in this.clientScriptExtensions.Keys) {
+ ErrorUtilities.VerifyArgument(ext != typeof(T), OpenIdStrings.ClientScriptExtensionTypeCollision, typeof(T).FullName);
+ }
+ this.clientScriptExtensions.Add(typeof(T), propertyName);
+ }
+
+ #region IDisposable Members
+
+ /// <summary>
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ /// </summary>
+ public void Dispose() {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Determines whether some parameter name belongs to OpenID or this library
+ /// as a protocol or internal parameter name.
+ /// </summary>
+ /// <param name="parameterName">Name of the parameter.</param>
+ /// <returns>
+ /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
+ /// </returns>
+ internal static bool IsOpenIdSupportingParameter(string parameterName) {
+ // Yes, it is possible with some query strings to have a null or empty parameter name
+ if (string.IsNullOrEmpty(parameterName)) {
+ return false;
+ }
+
+ Protocol protocol = Protocol.Default;
+ return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
+ || parameterName.StartsWith(OpenIdUtilities.CustomParameterPrefix, StringComparison.Ordinal);
+ }
+
+ /// <summary>
+ /// Creates a relying party that does not verify incoming messages against
+ /// nonce or association stores.
+ /// </summary>
+ /// <returns>The instantiated <see cref="OpenIdRelyingParty"/>.</returns>
+ /// <remarks>
+ /// Useful for previewing messages while
+ /// allowing them to be fully processed and verified later.
+ /// </remarks>
+ internal static OpenIdRelyingParty CreateNonVerifying() {
+ OpenIdRelyingParty rp = new OpenIdRelyingParty();
+ try {
+ rp.Channel = OpenIdChannel.CreateNonVerifyingChannel();
+ return rp;
+ } catch {
+ rp.Dispose();
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Processes the response received in a popup window or iframe to an AJAX-directed OpenID authentication.
+ /// </summary>
+ /// <param name="request">The incoming HTTP request that is expected to carry an OpenID authentication response.</param>
+ /// <param name="callback">The callback fired after the response status has been determined but before the Javascript response is formulated.</param>
+ /// <returns>
+ /// The HTTP response to send to this HTTP request.
+ /// </returns>
+ [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "OpenID", Justification = "real word"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "iframe", Justification = "Code contracts")]
+ internal OutgoingWebResponse ProcessResponseFromPopup(HttpRequestInfo request, Action<AuthenticationStatus> callback) {
+ Contract.Requires<ArgumentNullException>(request != null);
+ Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null);
+
+ string extensionsJson = null;
+ var authResponse = this.NonVerifyingRelyingParty.GetResponse();
+ ErrorUtilities.VerifyProtocol(authResponse != null, OpenIdStrings.PopupRedirectMissingResponse);
+
+ // Give the caller a chance to notify the hosting page and fill up the clientScriptExtensions collection.
+ if (callback != null) {
+ callback(authResponse.Status);
+ }
+
+ Logger.OpenId.DebugFormat("Popup or iframe callback from OP: {0}", request.Url);
+ Logger.Controls.DebugFormat(
+ "An authentication response was found in a popup window or iframe using a non-verifying RP with status: {0}",
+ authResponse.Status);
+ if (authResponse.Status == AuthenticationStatus.Authenticated) {
+ var extensionsDictionary = new Dictionary<string, string>();
+ foreach (var pair in this.clientScriptExtensions) {
+ IClientScriptExtensionResponse extension = (IClientScriptExtensionResponse)authResponse.GetExtension(pair.Key);
+ if (extension == null) {
+ continue;
+ }
+ var positiveResponse = (PositiveAuthenticationResponse)authResponse;
+ string js = extension.InitializeJavaScriptData(positiveResponse.Response);
+ if (!string.IsNullOrEmpty(js)) {
+ extensionsDictionary[pair.Value] = js;
+ }
+ }
+
+ extensionsJson = MessagingUtilities.CreateJsonObject(extensionsDictionary, true);
+ }
+
+ string payload = "document.URL";
+ if (request.HttpMethod == "POST") {
+ // Promote all form variables to the query string, but since it won't be passed
+ // to any server (this is a javascript window-to-window transfer) the length of
+ // it can be arbitrarily long, whereas it was POSTed here probably because it
+ // was too long for HTTP transit.
+ UriBuilder payloadUri = new UriBuilder(request.Url);
+ payloadUri.AppendQueryArgs(request.Form.ToDictionary());
+ payload = MessagingUtilities.GetSafeJavascriptValue(payloadUri.Uri.AbsoluteUri);
+ }
+
+ if (!string.IsNullOrEmpty(extensionsJson)) {
+ payload += ", " + extensionsJson;
+ }
+
+ return InvokeParentPageScript("dnoa_internal.processAuthorizationResult(" + payload + ")");
+ }
+
+ /// <summary>
+ /// Performs discovery on the specified identifier.
+ /// </summary>
+ /// <param name="identifier">The identifier to discover services for.</param>
+ /// <returns>A non-null sequence of services discovered for the identifier.</returns>
+ internal IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier) {
+ Contract.Requires<ArgumentNullException>(identifier != null);
+ Contract.Ensures(Contract.Result<IEnumerable<IdentifierDiscoveryResult>>() != null);
+
+ IEnumerable<IdentifierDiscoveryResult> results = Enumerable.Empty<IdentifierDiscoveryResult>();
+ foreach (var discoverer in this.DiscoveryServices) {
+ bool abortDiscoveryChain;
+ var discoveryResults = discoverer.Discover(identifier, this.WebRequestHandler, out abortDiscoveryChain).CacheGeneratedResults();
+ results = results.Concat(discoveryResults);
+ if (abortDiscoveryChain) {
+ Logger.OpenId.InfoFormat("Further discovery on '{0}' was stopped by the {1} discovery service.", identifier, discoverer.GetType().Name);
+ break;
+ }
+ }
+
+ // If any OP Identifier service elements were found, we must not proceed
+ // to use any Claimed Identifier services, per OpenID 2.0 sections 7.3.2.2 and 11.2.
+ // For a discussion on this topic, see
+ // http://groups.google.com/group/dotnetopenid/browse_thread/thread/4b5a8c6b2210f387/5e25910e4d2252c8
+ // Sometimes the IIdentifierDiscoveryService will automatically filter this for us, but
+ // just to be sure, we'll do it here as well.
+ if (!this.SecuritySettings.AllowDualPurposeIdentifiers) {
+ results = results.CacheGeneratedResults(); // avoid performing discovery repeatedly
+ var opIdentifiers = results.Where(result => result.ClaimedIdentifier == result.Protocol.ClaimedIdentifierForOPIdentifier);
+ var claimedIdentifiers = results.Where(result => result.ClaimedIdentifier != result.Protocol.ClaimedIdentifierForOPIdentifier);
+ results = opIdentifiers.Any() ? opIdentifiers : claimedIdentifiers;
+ }
+
+ return results;
+ }
+
+ /// <summary>
+ /// Checks whether a given OP Endpoint is permitted by the host relying party.
+ /// </summary>
+ /// <param name="endpoint">The OP endpoint.</param>
+ /// <returns><c>true</c> if the OP Endpoint is allowed; <c>false</c> otherwise.</returns>
+ protected internal bool FilterEndpoint(IProviderEndpoint endpoint) {
+ if (this.SecuritySettings.RejectAssertionsFromUntrustedProviders) {
+ if (!this.SecuritySettings.TrustedProviderEndpoints.Contains(endpoint.Uri)) {
+ Logger.OpenId.InfoFormat("Filtering out OP endpoint {0} because it is not on the exclusive trusted provider whitelist.", endpoint.Uri.AbsoluteUri);
+ return false;
+ }
+ }
+
+ if (endpoint.Version < Protocol.Lookup(this.SecuritySettings.MinimumRequiredOpenIdVersion).Version) {
+ Logger.OpenId.InfoFormat(
+ "Filtering out OP endpoint {0} because it implements OpenID {1} but this relying party requires OpenID {2} or later.",
+ endpoint.Uri.AbsoluteUri,
+ endpoint.Version,
+ Protocol.Lookup(this.SecuritySettings.MinimumRequiredOpenIdVersion).Version);
+ return false;
+ }
+
+ if (this.EndpointFilter != null) {
+ if (!this.EndpointFilter(endpoint)) {
+ Logger.OpenId.InfoFormat("Filtering out OP endpoint {0} because the host rejected it.", endpoint.Uri.AbsoluteUri);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing) {
+ if (disposing) {
+ if (this.nonVerifyingRelyingParty != null) {
+ this.nonVerifyingRelyingParty.Dispose();
+ this.nonVerifyingRelyingParty = null;
+ }
+
+ // Tear off the instance member as a local variable for thread safety.
+ IDisposable disposableChannel = this.channel as IDisposable;
+ if (disposableChannel != null) {
+ disposableChannel.Dispose();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame or window and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the parent window, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ /// <returns>The entire HTTP response to send to the popup window or iframe to perform the invocation.</returns>
+ private static OutgoingWebResponse InvokeParentPageScript(string methodCall) {
+ Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(methodCall));
+
+ Logger.OpenId.DebugFormat("Sending Javascript callback: {0}", methodCall);
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("<html><body><script type='text/javascript' language='javascript'><!--");
+ builder.AppendLine("//<![CDATA[");
+ builder.Append(@" var inPopup = !window.frameElement;
+ var objSrc = inPopup ? window.opener : window.frameElement;
+");
+
+ // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable,
+ // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already
+ // whether to call window.self.close() after the call.
+ string htmlFormat = @" if (inPopup) {{
+ try {{
+ objSrc.{0};
+ }} catch (ex) {{
+ alert(ex);
+ }} finally {{
+ window.self.close();
+ }}
+ }} else {{
+ objSrc.{0};
+ }}";
+ builder.AppendFormat(CultureInfo.InvariantCulture, htmlFormat, methodCall);
+ builder.AppendLine("//]]>--></script>");
+ builder.AppendLine("</body></html>");
+
+ var response = new OutgoingWebResponse();
+ response.Body = builder.ToString();
+ response.Headers.Add(HttpResponseHeader.ContentType, new ContentType("text/html").ToString());
+ return response;
+ }
+
+ /// <summary>
+ /// Called by derived classes when behaviors are added or removed.
+ /// </summary>
+ /// <param name="sender">The collection being modified.</param>
+ /// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
+ private void OnBehaviorsChanged(object sender, NotifyCollectionChangedEventArgs e) {
+ foreach (IRelyingPartyBehavior profile in e.NewItems) {
+ profile.ApplySecuritySettings(this.SecuritySettings);
+ Reporting.RecordFeatureUse(profile);
+ }
+ }
+
+#if CONTRACTS_FULL
+ /// <summary>
+ /// Verifies conditions that should be true for any valid state of this object.
+ /// </summary>
+ [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Called by code contracts.")]
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Called by code contracts.")]
+ [ContractInvariantMethod]
+ private void ObjectInvariant() {
+ Contract.Invariant(this.SecuritySettings != null);
+ Contract.Invariant(this.Channel != null);
+ Contract.Invariant(this.EndpointOrder != null);
+ }
+#endif
+ }
+}