Refactored association management functions from the OpenIdRelyingParty class into...
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / RelyingParty / OpenIdRelyingParty.cs
blob3b593b214d1db4de35838b78d1498b233ffc8397
1 //-----------------------------------------------------------------------
2 // <copyright file="OpenIdRelyingParty.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.Collections.Specialized;
11 using System.ComponentModel;
12 using System.Linq;
13 using System.Web;
14 using DotNetOpenAuth.Configuration;
15 using DotNetOpenAuth.Messaging;
16 using DotNetOpenAuth.Messaging.Bindings;
17 using DotNetOpenAuth.OpenId.ChannelElements;
18 using DotNetOpenAuth.OpenId.Messages;
20 /// <summary>
21 /// A delegate that decides whether a given OpenID Provider endpoint may be
22 /// considered for authenticating a user.
23 /// </summary>
24 /// <param name="endpoint">The endpoint for consideration.</param>
25 /// <returns>
26 /// <c>True</c> if the endpoint should be considered.
27 /// <c>False</c> to remove it from the pool of acceptable providers.
28 /// </returns>
29 public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint);
31 /// <summary>
32 /// Provides the programmatic facilities to act as an OpenId consumer.
33 /// </summary>
34 public sealed class OpenIdRelyingParty {
35 /// <summary>
36 /// The name of the key to use in the HttpApplication cache to store the
37 /// instance of <see cref="StandardRelyingPartyApplicationStore"/> to use.
38 /// </summary>
39 private const string ApplicationStoreKey = "DotNetOpenAuth.OpenId.RelyingParty.OpenIdRelyingParty.ApplicationStore";
41 /// <summary>
42 /// Backing field for the <see cref="SecuritySettings"/> property.
43 /// </summary>
44 private RelyingPartySecuritySettings securitySettings;
46 /// <summary>
47 /// Backing store for the <see cref="EndpointOrder"/> property.
48 /// </summary>
49 private Comparison<IXrdsProviderEndpoint> endpointOrder = DefaultEndpointOrder;
51 /// <summary>
52 /// Backing field for the <see cref="Channel"/> property.
53 /// </summary>
54 private Channel channel;
56 /// <summary>
57 /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
58 /// </summary>
59 public OpenIdRelyingParty()
60 : this(DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.ApplicationStore.CreateInstance(HttpApplicationStore)) {
63 /// <summary>
64 /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
65 /// </summary>
66 /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param>
67 public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore)
68 : this(applicationStore, applicationStore, applicationStore) {
71 /// <summary>
72 /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
73 /// </summary>
74 /// <param name="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
75 /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
76 /// <param name="secretStore">The secret store to use. If null, the relying party will always operate in "dumb mode".</param>
77 private OpenIdRelyingParty(IAssociationStore<Uri> associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) {
78 // If we are a smart-mode RP (supporting associations), then we MUST also be
79 // capable of storing nonces to prevent replay attacks.
80 // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
81 ErrorUtilities.VerifyArgument(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
83 this.securitySettings = DotNetOpenAuthSection.Configuration.OpenId.RelyingParty.SecuritySettings.CreateSecuritySettings();
85 // Without a nonce store, we must rely on the Provider to protect against
86 // replay attacks. But only 2.0+ Providers can be expected to provide
87 // replay protection.
88 if (nonceStore == null) {
89 this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
92 this.channel = new OpenIdChannel(associationStore, nonceStore, secretStore, this.SecuritySettings);
93 this.AssociationManager = new AssociationManager(this.Channel, associationStore, this.SecuritySettings);
96 /// <summary>
97 /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
98 /// attribute to determine order.
99 /// </summary>
100 /// <remarks>
101 /// Endpoints lacking any priority value are sorted to the end of the list.
102 /// </remarks>
103 [EditorBrowsable(EditorBrowsableState.Advanced)]
104 public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
105 get { return ServiceEndpoint.EndpointOrder; }
108 /// <summary>
109 /// Gets the standard state storage mechanism that uses ASP.NET's
110 /// HttpApplication state dictionary to store associations and nonces.
111 /// </summary>
112 [EditorBrowsable(EditorBrowsableState.Advanced)]
113 public static IRelyingPartyApplicationStore HttpApplicationStore {
114 get {
115 HttpContext context = HttpContext.Current;
116 ErrorUtilities.VerifyOperation(context != null, OpenIdStrings.StoreRequiredWhenNoHttpContextAvailable, typeof(IRelyingPartyApplicationStore).Name);
117 var store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey];
118 if (store == null) {
119 context.Application.Lock();
120 try {
121 if ((store = (IRelyingPartyApplicationStore)context.Application[ApplicationStoreKey]) == null) {
122 context.Application[ApplicationStoreKey] = store = new StandardRelyingPartyApplicationStore();
124 } finally {
125 context.Application.UnLock();
129 return store;
133 /// <summary>
134 /// Gets the channel to use for sending/receiving messages.
135 /// </summary>
136 public Channel Channel {
137 get {
138 return this.channel;
141 set {
142 ErrorUtilities.VerifyArgumentNotNull(value, "value");
143 this.channel = value;
144 this.AssociationManager.Channel = value;
148 /// <summary>
149 /// Gets the security settings used by this Relying Party.
150 /// </summary>
151 public RelyingPartySecuritySettings SecuritySettings {
152 get {
153 return this.securitySettings;
156 internal set {
157 ErrorUtilities.VerifyArgumentNotNull(value, "value");
158 this.securitySettings = value;
159 this.AssociationManager.SecuritySettings = value;
163 /// <summary>
164 /// Gets or sets the optional Provider Endpoint filter to use.
165 /// </summary>
166 /// <remarks>
167 /// Provides a way to optionally filter the providers that may be used in authenticating a user.
168 /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
169 /// If null, all identity providers will be accepted. This is the default.
170 /// </remarks>
171 [EditorBrowsable(EditorBrowsableState.Advanced)]
172 public EndpointSelector EndpointFilter { get; set; }
174 /// <summary>
175 /// Gets or sets the ordering routine that will determine which XRDS
176 /// Service element to try first
177 /// </summary>
178 /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
179 /// <remarks>
180 /// This may never be null. To reset to default behavior this property
181 /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
182 /// </remarks>
183 [EditorBrowsable(EditorBrowsableState.Advanced)]
184 public Comparison<IXrdsProviderEndpoint> EndpointOrder {
185 get {
186 return this.endpointOrder;
189 set {
190 ErrorUtilities.VerifyArgumentNotNull(value, "value");
191 this.endpointOrder = value;
195 /// <summary>
196 /// Gets a value indicating whether this Relying Party can sign its return_to
197 /// parameter in outgoing authentication requests.
198 /// </summary>
199 internal bool CanSignCallbackArguments {
200 get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); }
203 /// <summary>
204 /// Gets the web request handler to use for discovery and the part of
205 /// authentication where direct messages are sent to an untrusted remote party.
206 /// </summary>
207 internal IDirectWebRequestHandler WebRequestHandler {
208 get { return this.Channel.WebRequestHandler; }
211 /// <summary>
212 /// Gets the association manager.
213 /// </summary>
214 internal AssociationManager AssociationManager { get; private set; }
216 /// <summary>
217 /// Creates an authentication request to verify that a user controls
218 /// some given Identifier.
219 /// </summary>
220 /// <param name="userSuppliedIdentifier">
221 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
222 /// </param>
223 /// <param name="realm">
224 /// The shorest URL that describes this relying party web site's address.
225 /// For example, if your login page is found at https://www.example.com/login.aspx,
226 /// your realm would typically be https://www.example.com/.
227 /// </param>
228 /// <param name="returnToUrl">
229 /// The URL of the login page, or the page prepared to receive authentication
230 /// responses from the OpenID Provider.
231 /// </param>
232 /// <returns>
233 /// An authentication request object that describes the HTTP response to
234 /// send to the user agent to initiate the authentication.
235 /// </returns>
236 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
237 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
238 try {
239 return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
240 } catch (InvalidOperationException ex) {
241 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
245 /// <summary>
246 /// Creates an authentication request to verify that a user controls
247 /// some given Identifier.
248 /// </summary>
249 /// <param name="userSuppliedIdentifier">
250 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
251 /// </param>
252 /// <param name="realm">
253 /// The shorest URL that describes this relying party web site's address.
254 /// For example, if your login page is found at https://www.example.com/login.aspx,
255 /// your realm would typically be https://www.example.com/.
256 /// </param>
257 /// <returns>
258 /// An authentication request object that describes the HTTP response to
259 /// send to the user agent to initiate the authentication.
260 /// </returns>
261 /// <remarks>
262 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
263 /// </remarks>
264 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
265 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
266 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
267 try {
268 return this.CreateRequests(userSuppliedIdentifier, realm).First();
269 } catch (InvalidOperationException ex) {
270 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
274 /// <summary>
275 /// Creates an authentication request to verify that a user controls
276 /// some given Identifier.
277 /// </summary>
278 /// <param name="userSuppliedIdentifier">
279 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
280 /// </param>
281 /// <returns>
282 /// An authentication request object that describes the HTTP response to
283 /// send to the user agent to initiate the authentication.
284 /// </returns>
285 /// <remarks>
286 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
287 /// </remarks>
288 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
289 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
290 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
291 try {
292 return this.CreateRequests(userSuppliedIdentifier).First();
293 } catch (InvalidOperationException ex) {
294 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
298 /// <summary>
299 /// Gets an authentication response from a Provider.
300 /// </summary>
301 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
302 /// <remarks>
303 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
304 /// </remarks>
305 public IAuthenticationResponse GetResponse() {
306 return this.GetResponse(this.Channel.GetRequestFromContext());
309 /// <summary>
310 /// Gets an authentication response from a Provider.
311 /// </summary>
312 /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
313 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
314 public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
315 try {
316 var message = this.Channel.ReadFromRequest();
317 PositiveAssertionResponse positiveAssertion;
318 NegativeAssertionResponse negativeAssertion;
319 if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
320 return new PositiveAuthenticationResponse(positiveAssertion, this);
321 } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
322 return new NegativeAuthenticationResponse(negativeAssertion);
323 } else if (message != null) {
324 Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
327 return null;
328 } catch (ProtocolException ex) {
329 return new FailedAuthenticationResponse(ex);
333 /// <summary>
334 /// Determines whether some parameter name belongs to OpenID or this library
335 /// as a protocol or internal parameter name.
336 /// </summary>
337 /// <param name="parameterName">Name of the parameter.</param>
338 /// <returns>
339 /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
340 /// </returns>
341 internal static bool IsOpenIdSupportingParameter(string parameterName) {
342 Protocol protocol = Protocol.Default;
343 return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
344 || parameterName.StartsWith("dnoi.", StringComparison.Ordinal);
347 /// <summary>
348 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
349 /// </summary>
350 /// <param name="userSuppliedIdentifier">
351 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
352 /// </param>
353 /// <param name="realm">
354 /// The shorest URL that describes this relying party web site's address.
355 /// For example, if your login page is found at https://www.example.com/login.aspx,
356 /// your realm would typically be https://www.example.com/.
357 /// </param>
358 /// <param name="returnToUrl">
359 /// The URL of the login page, or the page prepared to receive authentication
360 /// responses from the OpenID Provider.
361 /// </param>
362 /// <returns>
363 /// An authentication request object that describes the HTTP response to
364 /// send to the user agent to initiate the authentication.
365 /// </returns>
366 /// <remarks>
367 /// <para>Any individual generated request can satisfy the authentication.
368 /// The generated requests are sorted in preferred order.
369 /// Each request is generated as it is enumerated to. Associations are created only as
370 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
371 /// <para>No exception is thrown if no OpenID endpoints were discovered.
372 /// An empty enumerable is returned instead.</para>
373 /// </remarks>
374 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
375 ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
376 ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl");
378 return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>();
381 /// <summary>
382 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
383 /// </summary>
384 /// <param name="userSuppliedIdentifier">
385 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
386 /// </param>
387 /// <param name="realm">
388 /// The shorest URL that describes this relying party web site's address.
389 /// For example, if your login page is found at https://www.example.com/login.aspx,
390 /// your realm would typically be https://www.example.com/.
391 /// </param>
392 /// <returns>
393 /// An authentication request object that describes the HTTP response to
394 /// send to the user agent to initiate the authentication.
395 /// </returns>
396 /// <remarks>
397 /// <para>Any individual generated request can satisfy the authentication.
398 /// The generated requests are sorted in preferred order.
399 /// Each request is generated as it is enumerated to. Associations are created only as
400 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
401 /// <para>No exception is thrown if no OpenID endpoints were discovered.
402 /// An empty enumerable is returned instead.</para>
403 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
404 /// </remarks>
405 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
406 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
407 ErrorUtilities.VerifyHttpContext();
409 // Build the return_to URL
410 UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
412 // Trim off any parameters with an "openid." prefix, and a few known others
413 // to avoid carrying state from a prior login attempt.
414 returnTo.Query = string.Empty;
415 NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC();
416 var returnToParams = new Dictionary<string, string>(queryParams.Count);
417 foreach (string key in queryParams) {
418 if (!IsOpenIdSupportingParameter(key)) {
419 returnToParams.Add(key, queryParams[key]);
422 returnTo.AppendQueryArgs(returnToParams);
424 return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
427 /// <summary>
428 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
429 /// </summary>
430 /// <param name="userSuppliedIdentifier">
431 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
432 /// </param>
433 /// <returns>
434 /// An authentication request object that describes the HTTP response to
435 /// send to the user agent to initiate the authentication.
436 /// </returns>
437 /// <remarks>
438 /// <para>Any individual generated request can satisfy the authentication.
439 /// The generated requests are sorted in preferred order.
440 /// Each request is generated as it is enumerated to. Associations are created only as
441 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
442 /// <para>No exception is thrown if no OpenID endpoints were discovered.
443 /// An empty enumerable is returned instead.</para>
444 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
445 /// </remarks>
446 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
447 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
448 ErrorUtilities.VerifyHttpContext();
450 // Build the realm URL
451 UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
452 realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
453 realmUrl.Query = null;
454 realmUrl.Fragment = null;
456 // For RP discovery, the realm url MUST NOT redirect. To prevent this for
457 // virtual directory hosted apps, we need to make sure that the realm path ends
458 // in a slash (since our calculation above guarantees it doesn't end in a specific
459 // page like default.aspx).
460 if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) {
461 realmUrl.Path += "/";
464 return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri));