summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs')
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs246
1 files changed, 246 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs
new file mode 100644
index 0000000..9a43506
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/OpenId/RelyingParty/AssociationManager.cs
@@ -0,0 +1,246 @@
+//-----------------------------------------------------------------------
+// <copyright file="AssociationManager.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OpenId.RelyingParty {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Net;
+ using System.Security;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OpenId.ChannelElements;
+ using DotNetOpenAuth.OpenId.Messages;
+
+ /// <summary>
+ /// Manages the establishment, storage and retrieval of associations at the relying party.
+ /// </summary>
+ internal class AssociationManager {
+ /// <summary>
+ /// The storage to use for saving and retrieving associations. May be null.
+ /// </summary>
+ private readonly IRelyingPartyAssociationStore associationStore;
+
+ /// <summary>
+ /// Backing field for the <see cref="Channel"/> property.
+ /// </summary>
+ private Channel channel;
+
+ /// <summary>
+ /// Backing field for the <see cref="SecuritySettings"/> property.
+ /// </summary>
+ private RelyingPartySecuritySettings securitySettings;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AssociationManager"/> class.
+ /// </summary>
+ /// <param name="channel">The channel the relying party is using.</param>
+ /// <param name="associationStore">The association store. May be null for dumb mode relying parties.</param>
+ /// <param name="securitySettings">The security settings.</param>
+ internal AssociationManager(Channel channel, IRelyingPartyAssociationStore associationStore, RelyingPartySecuritySettings securitySettings) {
+ Contract.Requires<ArgumentNullException>(channel != null);
+ Contract.Requires<ArgumentNullException>(securitySettings != null);
+
+ this.channel = channel;
+ this.associationStore = associationStore;
+ this.securitySettings = securitySettings;
+ }
+
+ /// <summary>
+ /// Gets or sets the channel to use for establishing associations.
+ /// </summary>
+ /// <value>The channel.</value>
+ internal Channel Channel {
+ get {
+ return this.channel;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.channel = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets or sets the security settings to apply in choosing association types to support.
+ /// </summary>
+ internal RelyingPartySecuritySettings SecuritySettings {
+ get {
+ return this.securitySettings;
+ }
+
+ set {
+ Contract.Requires<ArgumentNullException>(value != null);
+ this.securitySettings = value;
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance has an association store.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if the relying party can act in 'smart' mode;
+ /// <c>false</c> if the relying party must always act in 'dumb' mode.
+ /// </value>
+ internal bool HasAssociationStore {
+ get { return this.associationStore != null; }
+ }
+
+ /// <summary>
+ /// Gets the storage to use for saving and retrieving associations. May be null.
+ /// </summary>
+ internal IRelyingPartyAssociationStore AssociationStoreTestHook {
+ get { return this.associationStore; }
+ }
+
+ /// <summary>
+ /// Gets an association between this Relying Party and a given Provider
+ /// if it already exists in the association store.
+ /// </summary>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <returns>The association if one exists and has useful life remaining. Otherwise <c>null</c>.</returns>
+ internal Association GetExistingAssociation(IProviderEndpoint provider) {
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ // If the RP has no application store for associations, there's no point in creating one.
+ if (this.associationStore == null) {
+ return null;
+ }
+
+ Association association = this.associationStore.GetAssociation(provider.Uri, this.SecuritySettings);
+
+ // If the returned association does not fulfill security requirements, ignore it.
+ if (association != null && !this.SecuritySettings.IsAssociationInPermittedRange(association)) {
+ association = null;
+ }
+
+ if (association != null && !association.HasUsefulLifeRemaining) {
+ association = null;
+ }
+
+ return association;
+ }
+
+ /// <summary>
+ /// Gets an existing association with the specified Provider, or attempts to create
+ /// a new association of one does not already exist.
+ /// </summary>
+ /// <param name="provider">The provider to get an association for.</param>
+ /// <returns>The existing or new association; <c>null</c> if none existed and one could not be created.</returns>
+ internal Association GetOrCreateAssociation(IProviderEndpoint provider) {
+ return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider);
+ }
+
+ /// <summary>
+ /// Creates a new association with a given Provider.
+ /// </summary>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <returns>
+ /// The newly created association, or null if no association can be created with
+ /// the given Provider given the current security settings.
+ /// </returns>
+ /// <remarks>
+ /// A new association is created and returned even if one already exists in the
+ /// association store.
+ /// Any new association is automatically added to the <see cref="associationStore"/>.
+ /// </remarks>
+ private Association CreateNewAssociation(IProviderEndpoint provider) {
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ // If there is no association store, there is no point in creating an association.
+ if (this.associationStore == null) {
+ return null;
+ }
+
+ try {
+ var associateRequest = AssociateRequest.Create(this.securitySettings, provider);
+
+ const int RenegotiateRetries = 1;
+ return this.CreateNewAssociation(provider, associateRequest, RenegotiateRetries);
+ } catch (VerificationException ex) {
+ // See Trac ticket #163. In partial trust host environments, the
+ // Diffie-Hellman implementation we're using for HTTP OP endpoints
+ // sometimes causes the CLR to throw:
+ // "VerificationException: Operation could destabilize the runtime."
+ // Just give up and use dumb mode in this case.
+ Logger.OpenId.ErrorFormat("VerificationException occurred while trying to create an association with {0}. {1}", provider.Uri, ex);
+ return null;
+ }
+ }
+
+ /// <summary>
+ /// Creates a new association with a given Provider.
+ /// </summary>
+ /// <param name="provider">The provider to create an association with.</param>
+ /// <param name="associateRequest">The associate request. May be <c>null</c>, which will always result in a <c>null</c> return value..</param>
+ /// <param name="retriesRemaining">The number of times to try the associate request again if the Provider suggests it.</param>
+ /// <returns>
+ /// The newly created association, or null if no association can be created with
+ /// the given Provider given the current security settings.
+ /// </returns>
+ private Association CreateNewAssociation(IProviderEndpoint provider, AssociateRequest associateRequest, int retriesRemaining) {
+ Contract.Requires<ArgumentNullException>(provider != null);
+
+ if (associateRequest == null || retriesRemaining < 0) {
+ // this can happen if security requirements and protocol conflict
+ // to where there are no association types to choose from.
+ return null;
+ }
+
+ try {
+ var associateResponse = this.channel.Request(associateRequest);
+ var associateSuccessfulResponse = associateResponse as AssociateSuccessfulResponse;
+ var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse;
+ if (associateSuccessfulResponse != null) {
+ Association association = associateSuccessfulResponse.CreateAssociation(associateRequest, null, null);
+ this.associationStore.StoreAssociation(provider.Uri, association);
+ return association;
+ } else if (associateUnsuccessfulResponse != null) {
+ if (string.IsNullOrEmpty(associateUnsuccessfulResponse.AssociationType)) {
+ Logger.OpenId.Debug("Provider rejected an association request and gave no suggestion as to an alternative association type. Giving up.");
+ return null;
+ }
+
+ if (!this.securitySettings.IsAssociationInPermittedRange(Protocol.Lookup(provider.Version), associateUnsuccessfulResponse.AssociationType)) {
+ Logger.OpenId.DebugFormat("Provider rejected an association request and suggested '{0}' as an association to try, which this Relying Party does not support. Giving up.", associateUnsuccessfulResponse.AssociationType);
+ return null;
+ }
+
+ if (retriesRemaining <= 0) {
+ Logger.OpenId.Debug("Unable to agree on an association type with the Provider in the allowed number of retries. Giving up.");
+ return null;
+ }
+
+ // Make sure the Provider isn't suggesting an incompatible pair of association/session types.
+ Protocol protocol = Protocol.Lookup(provider.Version);
+ ErrorUtilities.VerifyProtocol(
+ HmacShaAssociation.IsDHSessionCompatible(protocol, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType),
+ OpenIdStrings.IncompatibleAssociationAndSessionTypes,
+ associateUnsuccessfulResponse.AssociationType,
+ associateUnsuccessfulResponse.SessionType);
+
+ associateRequest = AssociateRequest.Create(this.securitySettings, provider, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType);
+ return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1);
+ } else {
+ throw new ProtocolException(MessagingStrings.UnexpectedMessageReceivedOfMany);
+ }
+ } catch (ProtocolException ex) {
+ // If the association failed because the remote server can't handle Expect: 100 Continue headers,
+ // then our web request handler should have already accomodated for future calls. Go ahead and
+ // immediately make one of those future calls now to try to get the association to succeed.
+ if (StandardWebRequestHandler.IsExceptionFrom417ExpectationFailed(ex)) {
+ return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1);
+ }
+
+ // Since having associations with OPs is not totally critical, we'll log and eat
+ // the exception so that auth may continue in dumb mode.
+ Logger.OpenId.ErrorFormat("An error occurred while trying to create an association with {0}. {1}", provider.Uri, ex);
+ return null;
+ }
+ }
+ }
+}