Added sreg client javascript support for responses.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / RelyingParty / AssociationManager.cs
blob3ab290a87f163744944f75a2353d2c60665f23f6
1 //-----------------------------------------------------------------------
2 // <copyright file="AssociationManager.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
4 // </copyright>
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth.OpenId.RelyingParty {
8 using System;
9 using System.Collections.Generic;
10 using System.Linq;
11 using System.Text;
12 using DotNetOpenAuth.Messaging;
13 using DotNetOpenAuth.OpenId.ChannelElements;
14 using DotNetOpenAuth.OpenId.Messages;
16 /// <summary>
17 /// Manages the establishment, storage and retrieval of associations at the relying party.
18 /// </summary>
19 internal class AssociationManager {
20 /// <summary>
21 /// The storage to use for saving and retrieving associations. May be null.
22 /// </summary>
23 private readonly IAssociationStore<Uri> associationStore;
25 /// <summary>
26 /// Backing field for the <see cref="Channel"/> property.
27 /// </summary>
28 private Channel channel;
30 /// <summary>
31 /// Backing field for the <see cref="SecuritySettings"/> property.
32 /// </summary>
33 private RelyingPartySecuritySettings securitySettings;
35 /// <summary>
36 /// Initializes a new instance of the <see cref="AssociationManager"/> class.
37 /// </summary>
38 /// <param name="channel">The channel the relying party is using.</param>
39 /// <param name="associationStore">The association store. May be null for dumb mode relying parties.</param>
40 /// <param name="securitySettings">The security settings.</param>
41 internal AssociationManager(Channel channel, IAssociationStore<Uri> associationStore, RelyingPartySecuritySettings securitySettings) {
42 ErrorUtilities.VerifyArgumentNotNull(channel, "channel");
43 ErrorUtilities.VerifyArgumentNotNull(securitySettings, "securitySettings");
45 this.channel = channel;
46 this.associationStore = associationStore;
47 this.securitySettings = securitySettings;
50 /// <summary>
51 /// Gets or sets the channel to use for establishing associations.
52 /// </summary>
53 /// <value>The channel.</value>
54 internal Channel Channel {
55 get {
56 return this.channel;
59 set {
60 ErrorUtilities.VerifyArgumentNotNull(value, "value");
61 this.channel = value;
65 /// <summary>
66 /// Gets or sets the security settings to apply in choosing association types to support.
67 /// </summary>
68 internal RelyingPartySecuritySettings SecuritySettings {
69 get {
70 return this.securitySettings;
73 set {
74 ErrorUtilities.VerifyArgumentNotNull(value, "value");
75 this.securitySettings = value;
79 /// <summary>
80 /// Gets a value indicating whether this instance has an association store.
81 /// </summary>
82 /// <value>
83 /// <c>true</c> if the relying party can act in 'smart' mode;
84 /// <c>false</c> if the relying party must always act in 'dumb' mode.
85 /// </value>
86 internal bool HasAssociationStore {
87 get { return this.associationStore != null; }
90 /// <summary>
91 /// Gets an association between this Relying Party and a given Provider
92 /// if it already exists in the association store.
93 /// </summary>
94 /// <param name="provider">The provider to create an association with.</param>
95 /// <returns>The association if one exists and has useful life remaining. Otherwise <c>null</c>.</returns>
96 internal Association GetExistingAssociation(ProviderEndpointDescription provider) {
97 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
99 Protocol protocol = Protocol.Lookup(provider.ProtocolVersion);
101 // If the RP has no application store for associations, there's no point in creating one.
102 if (this.associationStore == null) {
103 return null;
106 // TODO: we need a way to lookup an association that fulfills a given set of security
107 // requirements. We may have a SHA-1 association and a SHA-256 association that need
108 // to be called for specifically. (a bizzare scenario, admittedly, making this low priority).
109 Association association = this.associationStore.GetAssociation(provider.Endpoint);
111 // If the returned association does not fulfill security requirements, ignore it.
112 if (association != null && !this.securitySettings.IsAssociationInPermittedRange(protocol, association.GetAssociationType(protocol))) {
113 association = null;
116 if (association != null && !association.HasUsefulLifeRemaining) {
117 association = null;
120 return association;
123 /// <summary>
124 /// Gets an existing association with the specified Provider, or attempts to create
125 /// a new association of one does not already exist.
126 /// </summary>
127 /// <param name="provider">The provider to get an association for.</param>
128 /// <returns>The existing or new association; <c>null</c> if none existed and one could not be created.</returns>
129 internal Association GetOrCreateAssociation(ProviderEndpointDescription provider) {
130 return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider);
133 /// <summary>
134 /// Creates a new association with a given Provider.
135 /// </summary>
136 /// <param name="provider">The provider to create an association with.</param>
137 /// <returns>
138 /// The newly created association, or null if no association can be created with
139 /// the given Provider given the current security settings.
140 /// </returns>
141 /// <remarks>
142 /// A new association is created and returned even if one already exists in the
143 /// association store.
144 /// Any new association is automatically added to the <see cref="associationStore"/>.
145 /// </remarks>
146 private Association CreateNewAssociation(ProviderEndpointDescription provider) {
147 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
149 // If there is no association store, there is no point in creating an association.
150 if (this.associationStore == null) {
151 return null;
154 var associateRequest = AssociateRequest.Create(this.securitySettings, provider);
156 const int RenegotiateRetries = 1;
157 return this.CreateNewAssociation(provider, associateRequest, RenegotiateRetries);
160 /// <summary>
161 /// Creates a new association with a given Provider.
162 /// </summary>
163 /// <param name="provider">The provider to create an association with.</param>
164 /// <param name="associateRequest">The associate request. May be <c>null</c>, which will always result in a <c>null</c> return value..</param>
165 /// <param name="retriesRemaining">The number of times to try the associate request again if the Provider suggests it.</param>
166 /// <returns>
167 /// The newly created association, or null if no association can be created with
168 /// the given Provider given the current security settings.
169 /// </returns>
170 private Association CreateNewAssociation(ProviderEndpointDescription provider, AssociateRequest associateRequest, int retriesRemaining) {
171 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
173 if (associateRequest == null || retriesRemaining < 0) {
174 // this can happen if security requirements and protocol conflict
175 // to where there are no association types to choose from.
176 return null;
179 try {
180 var associateResponse = this.channel.Request(associateRequest);
181 var associateSuccessfulResponse = associateResponse as AssociateSuccessfulResponse;
182 var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse;
183 if (associateSuccessfulResponse != null) {
184 Association association = associateSuccessfulResponse.CreateAssociation(associateRequest, null);
185 this.associationStore.StoreAssociation(provider.Endpoint, association);
186 return association;
187 } else if (associateUnsuccessfulResponse != null) {
188 if (string.IsNullOrEmpty(associateUnsuccessfulResponse.AssociationType)) {
189 Logger.Debug("Provider rejected an association request and gave no suggestion as to an alternative association type. Giving up.");
190 return null;
193 if (!this.securitySettings.IsAssociationInPermittedRange(Protocol.Lookup(provider.ProtocolVersion), associateUnsuccessfulResponse.AssociationType)) {
194 Logger.DebugFormat("Provider rejected an association request and suggested '{0}' as an association to try, which this Relying Party does not support. Giving up.");
195 return null;
198 if (retriesRemaining <= 0) {
199 Logger.Debug("Unable to agree on an association type with the Provider in the allowed number of retries. Giving up.");
200 return null;
203 // Make sure the Provider isn't suggesting an incompatible pair of association/session types.
204 Protocol protocol = Protocol.Lookup(provider.ProtocolVersion);
205 ErrorUtilities.VerifyProtocol(
206 HmacShaAssociation.IsDHSessionCompatible(protocol, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType),
207 OpenIdStrings.IncompatibleAssociationAndSessionTypes,
208 associateUnsuccessfulResponse.AssociationType,
209 associateUnsuccessfulResponse.SessionType);
211 associateRequest = AssociateRequest.Create(this.securitySettings, provider, associateUnsuccessfulResponse.AssociationType, associateUnsuccessfulResponse.SessionType);
212 return this.CreateNewAssociation(provider, associateRequest, retriesRemaining - 1);
213 } else {
214 throw new ProtocolException(MessagingStrings.UnexpectedMessageReceivedOfMany);
216 } catch (ProtocolException ex) {
217 // Since having associations with OPs is not totally critical, we'll log and eat
218 // the exception so that auth may continue in dumb mode.
219 Logger.ErrorFormat("An error occurred while trying to create an association with {0}. {1}", provider.Endpoint, ex);
220 return null;