diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs | 662 |
1 files changed, 662 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs new file mode 100644 index 0000000..58dfc2f --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Provider/OpenIdProvider.cs @@ -0,0 +1,662 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdProvider.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Provider { + 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.Linq; + using System.Threading; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + using RP = DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Offers services for a web page that is acting as an OpenID identity server. + /// </summary> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By design")] + [ContractVerification(true)] + public sealed class OpenIdProvider : IDisposable { + /// <summary> + /// The name of the key to use in the HttpApplication cache to store the + /// instance of <see cref="StandardProviderApplicationStore"/> to use. + /// </summary> + private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.Provider.OpenIdProvider.ApplicationStore"; + + /// <summary> + /// Backing store for the <see cref="Behaviors"/> property. + /// </summary> + private readonly ObservableCollection<IProviderBehavior> behaviors = new ObservableCollection<IProviderBehavior>(); + + /// <summary> + /// Backing field for the <see cref="SecuritySettings"/> property. + /// </summary> + private ProviderSecuritySettings securitySettings; + + /// <summary> + /// The relying party used to perform discovery on identifiers being sent in + /// unsolicited positive assertions. + /// </summary> + private RP.OpenIdRelyingParty relyingParty; + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProvider"/> class. + /// </summary> + public OpenIdProvider() + : this(DotNetOpenAuthSection.Configuration.OpenId.Provider.ApplicationStore.CreateInstance(HttpApplicationStore)) { + Contract.Ensures(this.SecuritySettings != null); + Contract.Ensures(this.Channel != null); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProvider"/> class. + /// </summary> + /// <param name="applicationStore">The application store to use. Cannot be null.</param> + public OpenIdProvider(IOpenIdApplicationStore applicationStore) + : this((INonceStore)applicationStore, (ICryptoKeyStore)applicationStore) { + Contract.Requires<ArgumentNullException>(applicationStore != null); + Contract.Ensures(this.SecuritySettings != null); + Contract.Ensures(this.Channel != null); + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIdProvider"/> class. + /// </summary> + /// <param name="nonceStore">The nonce store to use. Cannot be null.</param> + /// <param name="cryptoKeyStore">The crypto key store. Cannot be null.</param> + private OpenIdProvider(INonceStore nonceStore, ICryptoKeyStore cryptoKeyStore) { + Contract.Requires<ArgumentNullException>(nonceStore != null); + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Ensures(this.SecuritySettings != null); + Contract.Ensures(this.Channel != null); + + this.SecuritySettings = DotNetOpenAuthSection.Configuration.OpenId.Provider.SecuritySettings.CreateSecuritySettings(); + this.behaviors.CollectionChanged += this.OnBehaviorsChanged; + foreach (var behavior in DotNetOpenAuthSection.Configuration.OpenId.Provider.Behaviors.CreateInstances(false)) { + this.behaviors.Add(behavior); + } + + this.AssociationStore = new SwitchingAssociationStore(cryptoKeyStore, this.SecuritySettings); + this.Channel = new OpenIdChannel(this.AssociationStore, nonceStore, this.SecuritySettings); + this.CryptoKeyStore = cryptoKeyStore; + + Reporting.RecordFeatureAndDependencyUse(this, nonceStore); + } + + /// <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.Requires<InvalidOperationException>(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired); + Contract.Ensures(Contract.Result<IOpenIdApplicationStore>() != null); + HttpContext context = HttpContext.Current; + 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 StandardProviderApplicationStore(); + } + } finally { + context.Application.UnLock(); + } + } + + return store; + } + } + + /// <summary> + /// Gets the channel to use for sending/receiving messages. + /// </summary> + public Channel Channel { get; internal set; } + + /// <summary> + /// Gets the security settings used by this Provider. + /// </summary> + public ProviderSecuritySettings SecuritySettings { + get { + Contract.Ensures(Contract.Result<ProviderSecuritySettings>() != null); + Contract.Assume(this.securitySettings != null); + return this.securitySettings; + } + + internal set { + Contract.Requires<ArgumentNullException>(value != null); + this.securitySettings = value; + } + } + + /// <summary> + /// Gets the extension factories. + /// </summary> + public IList<IOpenIdExtensionFactory> ExtensionFactories { + get { return this.Channel.GetExtensionFactories(); } + } + + /// <summary> + /// Gets or sets the mechanism a host site can use to receive + /// notifications of errors when communicating with remote parties. + /// </summary> + public IErrorReporting ErrorReporting { get; set; } + + /// <summary> + /// Gets a list of custom behaviors to apply to OpenID actions. + /// </summary> + /// <remarks> + /// Adding behaviors can impact the security settings of the <see cref="OpenIdProvider"/> + /// in ways that subsequently removing the behaviors will not reverse. + /// </remarks> + public ICollection<IProviderBehavior> Behaviors { + get { return this.behaviors; } + } + + /// <summary> + /// Gets the crypto key store. + /// </summary> + public ICryptoKeyStore CryptoKeyStore { get; private set; } + + /// <summary> + /// Gets the association store. + /// </summary> + internal IProviderAssociationStore AssociationStore { get; private set; } + + /// <summary> + /// Gets the channel. + /// </summary> + internal OpenIdChannel OpenIdChannel { + get { return (OpenIdChannel)this.Channel; } + } + + /// <summary> + /// Gets the list of services that can perform discovery on identifiers given to this relying party. + /// </summary> + internal IList<IIdentifierDiscoveryService> DiscoveryServices { + get { return this.RelyingParty.DiscoveryServices; } + } + + /// <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 relying party used for discovery of identifiers sent in unsolicited assertions. + /// </summary> + private RP.OpenIdRelyingParty RelyingParty { + get { + if (this.relyingParty == null) { + lock (this) { + if (this.relyingParty == null) { + // we just need an RP that's capable of discovery, so stateless mode is fine. + this.relyingParty = new RP.OpenIdRelyingParty(null); + } + } + } + + this.relyingParty.Channel.WebRequestHandler = this.WebRequestHandler; + return this.relyingParty; + } + } + + /// <summary> + /// Gets the incoming OpenID request if there is one, or null if none was detected. + /// </summary> + /// <returns>The request that the hosting Provider should possibly process and then transmit the response for.</returns> + /// <remarks> + /// <para>Requests may be infrastructural to OpenID and allow auto-responses, or they may + /// be authentication requests where the Provider site has to make decisions based + /// on its own user database and policies.</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> + /// <exception cref="ProtocolException">Thrown if the incoming message is recognized but deviates from the protocol specification irrecoverably.</exception> + public IRequest GetRequest() { + return this.GetRequest(this.Channel.GetRequestFromContext()); + } + + /// <summary> + /// Gets the incoming OpenID request if there is one, or null if none was detected. + /// </summary> + /// <param name="httpRequestInfo">The incoming HTTP request to extract the message from.</param> + /// <returns> + /// The request that the hosting Provider should process and then transmit the response for. + /// Null if no valid OpenID request was detected in the given HTTP request. + /// </returns> + /// <remarks> + /// Requests may be infrastructural to OpenID and allow auto-responses, or they may + /// be authentication requests where the Provider site has to make decisions based + /// on its own user database and policies. + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the incoming message is recognized + /// but deviates from the protocol specification irrecoverably.</exception> + public IRequest GetRequest(HttpRequestInfo httpRequestInfo) { + Contract.Requires<ArgumentNullException>(httpRequestInfo != null); + IDirectedProtocolMessage incomingMessage = null; + + try { + incomingMessage = this.Channel.ReadFromRequest(httpRequestInfo); + if (incomingMessage == null) { + // If the incoming request does not resemble an OpenID message at all, + // it's probably a user who just navigated to this URL, and we should + // just return null so the host can display a message to the user. + if (httpRequestInfo.HttpMethod == "GET" && !httpRequestInfo.UrlBeforeRewriting.QueryStringContainPrefixedParameters(Protocol.Default.openid.Prefix)) { + return null; + } + + ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); + } + + IRequest result = null; + + var checkIdMessage = incomingMessage as CheckIdRequest; + if (checkIdMessage != null) { + result = new AuthenticationRequest(this, checkIdMessage); + } + + if (result == null) { + var extensionOnlyRequest = incomingMessage as SignedResponseRequest; + if (extensionOnlyRequest != null) { + result = new AnonymousRequest(this, extensionOnlyRequest); + } + } + + if (result == null) { + var checkAuthMessage = incomingMessage as CheckAuthenticationRequest; + if (checkAuthMessage != null) { + result = new AutoResponsiveRequest(incomingMessage, new CheckAuthenticationResponse(checkAuthMessage, this), this.SecuritySettings); + } + } + + if (result == null) { + var associateMessage = incomingMessage as AssociateRequest; + if (associateMessage != null) { + result = new AutoResponsiveRequest(incomingMessage, associateMessage.CreateResponse(this.AssociationStore, this.SecuritySettings), this.SecuritySettings); + } + } + + if (result != null) { + foreach (var behavior in this.Behaviors) { + if (behavior.OnIncomingRequest(result)) { + // This behavior matched this request. + break; + } + } + + return result; + } + + throw ErrorUtilities.ThrowProtocol(MessagingStrings.UnexpectedMessageReceivedOfMany); + } catch (ProtocolException ex) { + IRequest errorResponse = this.GetErrorResponse(ex, httpRequestInfo, incomingMessage); + if (errorResponse == null) { + throw; + } + + return errorResponse; + } + } + + /// <summary> + /// Sends the response to a received request. + /// </summary> + /// <param name="request">The incoming OpenID request whose response is to be sent.</param> + /// <exception cref="ThreadAbortException">Thrown by ASP.NET in order to prevent additional data from the page being sent to the client and corrupting the response.</exception> + /// <remarks> + /// <para>Requires an HttpContext.Current context. If one is not available, the caller should use + /// <see cref="PrepareResponse"/> instead and manually send the <see cref="OutgoingWebResponse"/> + /// to the client.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] + [EditorBrowsable(EditorBrowsableState.Never), Obsolete("Use the Respond method instead, and prepare for execution to continue on this page beyond the call to Respond.")] + public void SendResponse(IRequest request) { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentException>(request.IsResponseReady); + + this.ApplyBehaviorsToResponse(request); + Request requestInternal = (Request)request; + this.Channel.Send(requestInternal.Response); + } + + /// <summary> + /// Sends the response to a received request. + /// </summary> + /// <param name="request">The incoming OpenID request whose response is to be sent.</param> + /// <remarks> + /// <para>Requires an HttpContext.Current context. If one is not available, the caller should use + /// <see cref="PrepareResponse"/> instead and manually send the <see cref="OutgoingWebResponse"/> + /// to the client.</para> + /// </remarks> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] + public void Respond(IRequest request) { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.CurrentHttpContextRequired); + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentException>(request.IsResponseReady); + + this.ApplyBehaviorsToResponse(request); + Request requestInternal = (Request)request; + this.Channel.Respond(requestInternal.Response); + } + + /// <summary> + /// Gets the response to a received request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>The response that should be sent to the client.</returns> + /// <exception cref="InvalidOperationException">Thrown if <see cref="IRequest.IsResponseReady"/> is <c>false</c>.</exception> + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Code Contract requires that we cast early.")] + public OutgoingWebResponse PrepareResponse(IRequest request) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentException>(request.IsResponseReady); + + this.ApplyBehaviorsToResponse(request); + Request requestInternal = (Request)request; + return this.Channel.PrepareResponse(requestInternal.Response); + } + + /// <summary> + /// Sends an identity assertion on behalf of one of this Provider's + /// members in order to redirect the user agent to a relying party + /// web site and log him/her in immediately in one uninterrupted step. + /// </summary> + /// <param name="providerEndpoint">The absolute URL on the Provider site that receives OpenID messages.</param> + /// <param name="relyingPartyRealm">The URL of the Relying Party web site. + /// This will typically be the home page, but may be a longer URL if + /// that Relying Party considers the scope of its realm to be more specific. + /// The URL provided here must allow discovery of the Relying Party's + /// XRDS document that advertises its OpenID RP endpoint.</param> + /// <param name="claimedIdentifier">The Identifier you are asserting your member controls.</param> + /// <param name="localIdentifier">The Identifier you know your user by internally. This will typically + /// be the same as <paramref name="claimedIdentifier"/>.</param> + /// <param name="extensions">The extensions.</param> + public void SendUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { + Contract.Requires<InvalidOperationException>(HttpContext.Current != null, MessagingStrings.HttpContextRequired); + Contract.Requires<ArgumentNullException>(providerEndpoint != null); + Contract.Requires<ArgumentException>(providerEndpoint.IsAbsoluteUri); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); + Contract.Requires<ArgumentNullException>(claimedIdentifier != null); + Contract.Requires<ArgumentNullException>(localIdentifier != null); + + this.PrepareUnsolicitedAssertion(providerEndpoint, relyingPartyRealm, claimedIdentifier, localIdentifier, extensions).Respond(); + } + + /// <summary> + /// Prepares an identity assertion on behalf of one of this Provider's + /// members in order to redirect the user agent to a relying party + /// web site and log him/her in immediately in one uninterrupted step. + /// </summary> + /// <param name="providerEndpoint">The absolute URL on the Provider site that receives OpenID messages.</param> + /// <param name="relyingPartyRealm">The URL of the Relying Party web site. + /// This will typically be the home page, but may be a longer URL if + /// that Relying Party considers the scope of its realm to be more specific. + /// The URL provided here must allow discovery of the Relying Party's + /// XRDS document that advertises its OpenID RP endpoint.</param> + /// <param name="claimedIdentifier">The Identifier you are asserting your member controls.</param> + /// <param name="localIdentifier">The Identifier you know your user by internally. This will typically + /// be the same as <paramref name="claimedIdentifier"/>.</param> + /// <param name="extensions">The extensions.</param> + /// <returns> + /// A <see cref="OutgoingWebResponse"/> object describing the HTTP response to send + /// the user agent to allow the redirect with assertion to happen. + /// </returns> + public OutgoingWebResponse PrepareUnsolicitedAssertion(Uri providerEndpoint, Realm relyingPartyRealm, Identifier claimedIdentifier, Identifier localIdentifier, params IExtensionMessage[] extensions) { + Contract.Requires<ArgumentNullException>(providerEndpoint != null); + Contract.Requires<ArgumentException>(providerEndpoint.IsAbsoluteUri); + Contract.Requires<ArgumentNullException>(relyingPartyRealm != null); + Contract.Requires<ArgumentNullException>(claimedIdentifier != null); + Contract.Requires<ArgumentNullException>(localIdentifier != null); + Contract.Requires<InvalidOperationException>(this.Channel.WebRequestHandler != null); + + // Although the RP should do their due diligence to make sure that this OP + // is authorized to send an assertion for the given claimed identifier, + // do due diligence by performing our own discovery on the claimed identifier + // and make sure that it is tied to this OP and OP local identifier. + if (this.SecuritySettings.UnsolicitedAssertionVerification != ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.NeverVerify) { + var serviceEndpoint = IdentifierDiscoveryResult.CreateForClaimedIdentifier(claimedIdentifier, localIdentifier, new ProviderEndpointDescription(providerEndpoint, Protocol.Default.Version), null, null); + var discoveredEndpoints = this.RelyingParty.Discover(claimedIdentifier); + if (!discoveredEndpoints.Contains(serviceEndpoint)) { + Logger.OpenId.WarnFormat( + "Failed to send unsolicited assertion for {0} because its discovered services did not include this endpoint: {1}{2}{1}Discovered endpoints: {1}{3}", + claimedIdentifier, + Environment.NewLine, + serviceEndpoint, + discoveredEndpoints.ToStringDeferred(true)); + + // Only FAIL if the setting is set for it. + if (this.securitySettings.UnsolicitedAssertionVerification == ProviderSecuritySettings.UnsolicitedAssertionVerificationLevel.RequireSuccess) { + ErrorUtilities.ThrowProtocol(OpenIdStrings.UnsolicitedAssertionForUnrelatedClaimedIdentifier, claimedIdentifier); + } + } + } + + Logger.OpenId.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier); + RelyingPartyEndpointDescription returnToEndpoint = null; + var returnToEndpoints = relyingPartyRealm.DiscoverReturnToEndpoints(this.WebRequestHandler, true); + if (returnToEndpoints != null) { + returnToEndpoint = returnToEndpoints.FirstOrDefault(); + } + ErrorUtilities.VerifyProtocol(returnToEndpoint != null, OpenIdStrings.NoRelyingPartyEndpointDiscovered, relyingPartyRealm); + + var positiveAssertion = new PositiveAssertionResponse(returnToEndpoint) { + ProviderEndpoint = providerEndpoint, + ClaimedIdentifier = claimedIdentifier, + LocalIdentifier = localIdentifier, + }; + + if (extensions != null) { + foreach (IExtensionMessage extension in extensions) { + positiveAssertion.Extensions.Add(extension); + } + } + + Reporting.RecordEventOccurrence(this, "PrepareUnsolicitedAssertion"); + return this.Channel.PrepareResponse(positiveAssertion); + } + + #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); + } + + /// <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> + private void Dispose(bool disposing) { + if (disposing) { + // Tear off the instance member as a local variable for thread safety. + IDisposable channel = this.Channel as IDisposable; + if (channel != null) { + channel.Dispose(); + } + + if (this.relyingParty != null) { + this.relyingParty.Dispose(); + } + } + } + + #endregion + + /// <summary> + /// Applies all behaviors to the response message. + /// </summary> + /// <param name="request">The request.</param> + private void ApplyBehaviorsToResponse(IRequest request) { + var authRequest = request as IAuthenticationRequest; + if (authRequest != null) { + foreach (var behavior in this.Behaviors) { + if (behavior.OnOutgoingResponse(authRequest)) { + // This behavior matched this request. + break; + } + } + } + } + + /// <summary> + /// Prepares the return value for the GetRequest method in the event of an exception. + /// </summary> + /// <param name="ex">The exception that forms the basis of the error response. Must not be null.</param> + /// <param name="httpRequestInfo">The incoming HTTP request. Must not be null.</param> + /// <param name="incomingMessage">The incoming message. May be null in the case that it was malformed.</param> + /// <returns> + /// Either the <see cref="IRequest"/> to return to the host site or null to indicate no response could be reasonably created and that the caller should rethrow the exception. + /// </returns> + private IRequest GetErrorResponse(ProtocolException ex, HttpRequestInfo httpRequestInfo, IDirectedProtocolMessage incomingMessage) { + Contract.Requires<ArgumentNullException>(ex != null); + Contract.Requires<ArgumentNullException>(httpRequestInfo != null); + + Logger.OpenId.Error("An exception was generated while processing an incoming OpenID request.", ex); + IErrorMessage errorMessage; + + // We must create the appropriate error message type (direct vs. indirect) + // based on what we see in the request. + string returnTo = httpRequestInfo.QueryString[Protocol.Default.openid.return_to]; + if (returnTo != null) { + // An indirect request message from the RP + // We need to return an indirect response error message so the RP can consume it. + // Consistent with OpenID 2.0 section 5.2.3. + var indirectRequest = incomingMessage as SignedResponseRequest; + if (indirectRequest != null) { + errorMessage = new IndirectErrorResponse(indirectRequest); + } else { + errorMessage = new IndirectErrorResponse(Protocol.Default.Version, new Uri(returnTo)); + } + } else if (httpRequestInfo.HttpMethod == "POST") { + // A direct request message from the RP + // We need to return a direct response error message so the RP can consume it. + // Consistent with OpenID 2.0 section 5.1.2.2. + errorMessage = new DirectErrorResponse(Protocol.Default.Version, incomingMessage); + } else { + // This may be an indirect request from an RP that was so badly + // formed that we cannot even return an error to the RP. + // The best we can do is display an error to the user. + // Returning null cues the caller to "throw;" + return null; + } + + errorMessage.ErrorMessage = ex.ToStringDescriptive(); + + // Allow host to log this error and issue a ticket #. + // We tear off the field to a local var for thread safety. + IErrorReporting hostErrorHandler = this.ErrorReporting; + if (hostErrorHandler != null) { + errorMessage.Contact = hostErrorHandler.Contact; + errorMessage.Reference = hostErrorHandler.LogError(ex); + } + + if (incomingMessage != null) { + return new AutoResponsiveRequest(incomingMessage, errorMessage, this.SecuritySettings); + } else { + return new AutoResponsiveRequest(errorMessage, this.SecuritySettings); + } + } + + /// <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 (IProviderBehavior profile in e.NewItems) { + profile.ApplySecuritySettings(this.SecuritySettings); + Reporting.RecordFeatureUse(profile); + } + } + + /// <summary> + /// Provides a single OP association store instance that can handle switching between + /// association handle encoding modes. + /// </summary> + private class SwitchingAssociationStore : IProviderAssociationStore { + /// <summary> + /// The security settings of the Provider. + /// </summary> + private readonly ProviderSecuritySettings securitySettings; + + /// <summary> + /// The association store that records association secrets in the association handles themselves. + /// </summary> + private IProviderAssociationStore associationHandleEncoder; + + /// <summary> + /// The association store that records association secrets in a secret store. + /// </summary> + private IProviderAssociationStore associationSecretStorage; + + /// <summary> + /// Initializes a new instance of the <see cref="SwitchingAssociationStore"/> class. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + /// <param name="securitySettings">The security settings.</param> + internal SwitchingAssociationStore(ICryptoKeyStore cryptoKeyStore, ProviderSecuritySettings securitySettings) { + Contract.Requires<ArgumentNullException>(cryptoKeyStore != null); + Contract.Requires<ArgumentNullException>(securitySettings != null); + this.securitySettings = securitySettings; + + this.associationHandleEncoder = new ProviderAssociationHandleEncoder(cryptoKeyStore); + this.associationSecretStorage = new ProviderAssociationKeyStorage(cryptoKeyStore); + } + + /// <summary> + /// Gets the association store that applies given the Provider's current security settings. + /// </summary> + internal IProviderAssociationStore AssociationStore { + get { return this.securitySettings.EncodeAssociationSecretsInHandles ? this.associationHandleEncoder : this.associationSecretStorage; } + } + + /// <summary> + /// Stores an association and returns a handle for it. + /// </summary> + /// <param name="secret">The association secret.</param> + /// <param name="expiresUtc">The UTC time that the association should expire.</param> + /// <param name="privateAssociation">A value indicating whether this is a private association.</param> + /// <returns> + /// The association handle that represents this association. + /// </returns> + public string Serialize(byte[] secret, DateTime expiresUtc, bool privateAssociation) { + return this.AssociationStore.Serialize(secret, expiresUtc, privateAssociation); + } + + /// <summary> + /// Retrieves an association given an association handle. + /// </summary> + /// <param name="containingMessage">The OpenID message that referenced this association handle.</param> + /// <param name="isPrivateAssociation">A value indicating whether a private association is expected.</param> + /// <param name="handle">The association handle.</param> + /// <returns> + /// An association instance, or <c>null</c> if the association has expired or the signature is incorrect (which may be because the OP's symmetric key has changed). + /// </returns> + /// <exception cref="ProtocolException">Thrown if the association is not of the expected type.</exception> + public Association Deserialize(IProtocolMessage containingMessage, bool isPrivateAssociation, string handle) { + return this.AssociationStore.Deserialize(containingMessage, isPrivateAssociation, handle); + } + } + } +} |