Added StandardProviderApplicationStore and several OpenIdProvider unit tests.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / RelyingParty / OpenIdRelyingParty.cs
blob48d51a83b260f8b047092eb4549cafc68ed339a4
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 /// Backing field for the <see cref="SecuritySettings"/> property.
37 /// </summary>
38 private RelyingPartySecuritySettings securitySettings;
40 /// <summary>
41 /// Backing store for the <see cref="EndpointOrder"/> property.
42 /// </summary>
43 private Comparison<IXrdsProviderEndpoint> endpointOrder = DefaultEndpointOrder;
45 /// <summary>
46 /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
47 /// </summary>
48 /// <param name="applicationStore">The application store. If null, the relying party will always operate in "dumb mode".</param>
49 public OpenIdRelyingParty(IRelyingPartyApplicationStore applicationStore)
50 : this(applicationStore, applicationStore, applicationStore) {
53 /// <summary>
54 /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
55 /// </summary>
56 /// <param name="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
57 /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
58 /// <param name="secretStore">The secret store to use. If null, the relying party will always operate in "dumb mode".</param>
59 private OpenIdRelyingParty(IAssociationStore<Uri> associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) {
60 // If we are a smart-mode RP (supporting associations), then we MUST also be
61 // capable of storing nonces to prevent replay attacks.
62 // If we're a dumb-mode RP, then 2.0 OPs are responsible for preventing replays.
63 ErrorUtilities.VerifyArgument(associationStore == null || nonceStore != null, OpenIdStrings.AssociationStoreRequiresNonceStore);
65 this.Channel = new OpenIdChannel(associationStore, nonceStore, secretStore);
66 this.AssociationStore = associationStore;
67 this.SecuritySettings = RelyingPartySection.Configuration.SecuritySettings.CreateSecuritySettings();
69 // Without a nonce store, we must rely on the Provider to protect against
70 // replay attacks. But only 2.0+ Providers can be expected to provide
71 // replay protection.
72 if (nonceStore == null) {
73 this.SecuritySettings.MinimumRequiredOpenIdVersion = ProtocolVersion.V20;
77 /// <summary>
78 /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
79 /// attribute to determine order.
80 /// </summary>
81 /// <remarks>
82 /// Endpoints lacking any priority value are sorted to the end of the list.
83 /// </remarks>
84 [EditorBrowsable(EditorBrowsableState.Advanced)]
85 public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
86 get {
87 // Sort first by service type (OpenID 2.0, 1.1, 1.0),
88 // then by Service/@priority, then by Service/Uri/@priority
89 return (se1, se2) => {
90 int result = GetEndpointPrecedenceOrderByServiceType(se1).CompareTo(GetEndpointPrecedenceOrderByServiceType(se2));
91 if (result != 0) {
92 return result;
94 if (se1.ServicePriority.HasValue && se2.ServicePriority.HasValue) {
95 result = se1.ServicePriority.Value.CompareTo(se2.ServicePriority.Value);
96 if (result != 0) {
97 return result;
99 if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) {
100 return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value);
101 } else if (se1.UriPriority.HasValue) {
102 return -1;
103 } else if (se2.UriPriority.HasValue) {
104 return 1;
105 } else {
106 return 0;
108 } else {
109 if (se1.ServicePriority.HasValue) {
110 return -1;
111 } else if (se2.ServicePriority.HasValue) {
112 return 1;
113 } else {
114 // neither service defines a priority, so base ordering by uri priority.
115 if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) {
116 return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value);
117 } else if (se1.UriPriority.HasValue) {
118 return -1;
119 } else if (se2.UriPriority.HasValue) {
120 return 1;
121 } else {
122 return 0;
130 /// <summary>
131 /// Gets the channel to use for sending/receiving messages.
132 /// </summary>
133 public Channel Channel { get; internal set; }
135 /// <summary>
136 /// Gets the security settings used by this Relying Party.
137 /// </summary>
138 public RelyingPartySecuritySettings SecuritySettings {
139 get {
140 return this.securitySettings;
143 internal set {
144 if (value == null) {
145 throw new ArgumentNullException("value");
148 this.securitySettings = value;
152 /// <summary>
153 /// Gets or sets the optional Provider Endpoint filter to use.
154 /// </summary>
155 /// <remarks>
156 /// Provides a way to optionally filter the providers that may be used in authenticating a user.
157 /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
158 /// If null, all identity providers will be accepted. This is the default.
159 /// </remarks>
160 [EditorBrowsable(EditorBrowsableState.Advanced)]
161 public EndpointSelector EndpointFilter { get; set; }
163 /// <summary>
164 /// Gets or sets the ordering routine that will determine which XRDS
165 /// Service element to try first
166 /// </summary>
167 /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
168 /// <remarks>
169 /// This may never be null. To reset to default behavior this property
170 /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
171 /// </remarks>
172 [EditorBrowsable(EditorBrowsableState.Advanced)]
173 public Comparison<IXrdsProviderEndpoint> EndpointOrder {
174 get {
175 return this.endpointOrder;
178 set {
179 ErrorUtilities.VerifyArgumentNotNull(value, "value");
180 this.endpointOrder = value;
184 /// <summary>
185 /// Gets the association store.
186 /// </summary>
187 internal IAssociationStore<Uri> AssociationStore { get; private set; }
189 /// <summary>
190 /// Gets a value indicating whether this Relying Party can sign its return_to
191 /// parameter in outgoing authentication requests.
192 /// </summary>
193 internal bool CanSignCallbackArguments {
194 get { return this.Channel.BindingElements.OfType<ReturnToSignatureBindingElement>().Any(); }
197 /// <summary>
198 /// Gets the web request handler to use for discovery and the part of
199 /// authentication where direct messages are sent to an untrusted remote party.
200 /// </summary>
201 internal IDirectWebRequestHandler WebRequestHandler {
202 get { return this.Channel.WebRequestHandler; }
205 /// <summary>
206 /// Creates an authentication request to verify that a user controls
207 /// some given Identifier.
208 /// </summary>
209 /// <param name="userSuppliedIdentifier">
210 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
211 /// </param>
212 /// <param name="realm">
213 /// The shorest URL that describes this relying party web site's address.
214 /// For example, if your login page is found at https://www.example.com/login.aspx,
215 /// your realm would typically be https://www.example.com/.
216 /// </param>
217 /// <param name="returnToUrl">
218 /// The URL of the login page, or the page prepared to receive authentication
219 /// responses from the OpenID Provider.
220 /// </param>
221 /// <returns>
222 /// An authentication request object that describes the HTTP response to
223 /// send to the user agent to initiate the authentication.
224 /// </returns>
225 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
226 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
227 try {
228 return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
229 } catch (InvalidOperationException ex) {
230 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
234 /// <summary>
235 /// Creates an authentication request to verify that a user controls
236 /// some given Identifier.
237 /// </summary>
238 /// <param name="userSuppliedIdentifier">
239 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
240 /// </param>
241 /// <param name="realm">
242 /// The shorest URL that describes this relying party web site's address.
243 /// For example, if your login page is found at https://www.example.com/login.aspx,
244 /// your realm would typically be https://www.example.com/.
245 /// </param>
246 /// <returns>
247 /// An authentication request object that describes the HTTP response to
248 /// send to the user agent to initiate the authentication.
249 /// </returns>
250 /// <remarks>
251 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
252 /// </remarks>
253 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
254 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
255 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
256 try {
257 return this.CreateRequests(userSuppliedIdentifier, realm).First();
258 } catch (InvalidOperationException ex) {
259 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
263 /// <summary>
264 /// Creates an authentication request to verify that a user controls
265 /// some given Identifier.
266 /// </summary>
267 /// <param name="userSuppliedIdentifier">
268 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
269 /// </param>
270 /// <returns>
271 /// An authentication request object that describes the HTTP response to
272 /// send to the user agent to initiate the authentication.
273 /// </returns>
274 /// <remarks>
275 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
276 /// </remarks>
277 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
278 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
279 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
280 try {
281 return this.CreateRequests(userSuppliedIdentifier).First();
282 } catch (InvalidOperationException ex) {
283 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
287 /// <summary>
288 /// Gets an authentication response from a Provider.
289 /// </summary>
290 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
291 /// <remarks>
292 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
293 /// </remarks>
294 public IAuthenticationResponse GetResponse() {
295 return this.GetResponse(this.Channel.GetRequestFromContext());
298 /// <summary>
299 /// Gets an authentication response from a Provider.
300 /// </summary>
301 /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
302 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
303 public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
304 try {
305 var message = this.Channel.ReadFromRequest();
306 PositiveAssertionResponse positiveAssertion;
307 NegativeAssertionResponse negativeAssertion;
308 if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
309 return new PositiveAuthenticationResponse(positiveAssertion, this);
310 } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
311 return new NegativeAuthenticationResponse(negativeAssertion);
312 } else if (message != null) {
313 Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
316 return null;
317 } catch (ProtocolException ex) {
318 return new FailedAuthenticationResponse(ex);
322 /// <summary>
323 /// Determines whether some parameter name belongs to OpenID or this library
324 /// as a protocol or internal parameter name.
325 /// </summary>
326 /// <param name="parameterName">Name of the parameter.</param>
327 /// <returns>
328 /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
329 /// </returns>
330 internal static bool IsOpenIdSupportingParameter(string parameterName) {
331 Protocol protocol = Protocol.Default;
332 return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
333 || parameterName.StartsWith("dnoi.", StringComparison.Ordinal);
336 /// <summary>
337 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
338 /// </summary>
339 /// <param name="userSuppliedIdentifier">
340 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
341 /// </param>
342 /// <param name="realm">
343 /// The shorest URL that describes this relying party web site's address.
344 /// For example, if your login page is found at https://www.example.com/login.aspx,
345 /// your realm would typically be https://www.example.com/.
346 /// </param>
347 /// <param name="returnToUrl">
348 /// The URL of the login page, or the page prepared to receive authentication
349 /// responses from the OpenID Provider.
350 /// </param>
351 /// <returns>
352 /// An authentication request object that describes the HTTP response to
353 /// send to the user agent to initiate the authentication.
354 /// </returns>
355 /// <remarks>
356 /// <para>Any individual generated request can satisfy the authentication.
357 /// The generated requests are sorted in preferred order.
358 /// Each request is generated as it is enumerated to. Associations are created only as
359 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
360 /// <para>No exception is thrown if no OpenID endpoints were discovered.
361 /// An empty enumerable is returned instead.</para>
362 /// </remarks>
363 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
364 ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
365 ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl");
367 return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl, true).Cast<IAuthenticationRequest>();
370 /// <summary>
371 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
372 /// </summary>
373 /// <param name="userSuppliedIdentifier">
374 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
375 /// </param>
376 /// <param name="realm">
377 /// The shorest URL that describes this relying party web site's address.
378 /// For example, if your login page is found at https://www.example.com/login.aspx,
379 /// your realm would typically be https://www.example.com/.
380 /// </param>
381 /// <returns>
382 /// An authentication request object that describes the HTTP response to
383 /// send to the user agent to initiate the authentication.
384 /// </returns>
385 /// <remarks>
386 /// <para>Any individual generated request can satisfy the authentication.
387 /// The generated requests are sorted in preferred order.
388 /// Each request is generated as it is enumerated to. Associations are created only as
389 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
390 /// <para>No exception is thrown if no OpenID endpoints were discovered.
391 /// An empty enumerable is returned instead.</para>
392 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
393 /// </remarks>
394 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
395 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
396 ErrorUtilities.VerifyHttpContext();
398 // Build the return_to URL
399 UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
401 // Trim off any parameters with an "openid." prefix, and a few known others
402 // to avoid carrying state from a prior login attempt.
403 returnTo.Query = string.Empty;
404 NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC();
405 var returnToParams = new Dictionary<string, string>(queryParams.Count);
406 foreach (string key in queryParams) {
407 if (!IsOpenIdSupportingParameter(key)) {
408 returnToParams.Add(key, queryParams[key]);
411 returnTo.AppendQueryArgs(returnToParams);
413 return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
416 /// <summary>
417 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
418 /// </summary>
419 /// <param name="userSuppliedIdentifier">
420 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
421 /// </param>
422 /// <returns>
423 /// An authentication request object that describes the HTTP response to
424 /// send to the user agent to initiate the authentication.
425 /// </returns>
426 /// <remarks>
427 /// <para>Any individual generated request can satisfy the authentication.
428 /// The generated requests are sorted in preferred order.
429 /// Each request is generated as it is enumerated to. Associations are created only as
430 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
431 /// <para>No exception is thrown if no OpenID endpoints were discovered.
432 /// An empty enumerable is returned instead.</para>
433 /// <para>Requires an <see cref="HttpContext.Current">HttpContext.Current</see> context.</para>
434 /// </remarks>
435 /// <exception cref="InvalidOperationException">Thrown if <see cref="HttpContext.Current">HttpContext.Current</see> == <c>null</c>.</exception>
436 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
437 ErrorUtilities.VerifyHttpContext();
439 // Build the realm URL
440 UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
441 realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
442 realmUrl.Query = null;
443 realmUrl.Fragment = null;
445 // For RP discovery, the realm url MUST NOT redirect. To prevent this for
446 // virtual directory hosted apps, we need to make sure that the realm path ends
447 // in a slash (since our calculation above guarantees it doesn't end in a specific
448 // page like default.aspx).
449 if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) {
450 realmUrl.Path += "/";
453 return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri));
456 /// <summary>
457 /// Gets an association between this Relying Party and a given Provider
458 /// if it already exists in the association store.
459 /// </summary>
460 /// <param name="provider">The provider to create an association with.</param>
461 /// <returns>The association if one exists and has useful life remaining. Otherwise <c>null</c>.</returns>
462 internal Association GetExistingAssociation(ProviderEndpointDescription provider) {
463 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
465 Protocol protocol = Protocol.Lookup(provider.ProtocolVersion);
467 // If the RP has no application store for associations, there's no point in creating one.
468 if (this.AssociationStore == null) {
469 return null;
472 // TODO: we need a way to lookup an association that fulfills a given set of security
473 // requirements. We may have a SHA-1 association and a SHA-256 association that need
474 // to be called for specifically. (a bizzare scenario, admittedly, making this low priority).
475 Association association = this.AssociationStore.GetAssociation(provider.Endpoint);
477 // If the returned association does not fulfill security requirements, ignore it.
478 if (association != null && !this.SecuritySettings.IsAssociationInPermittedRange(protocol, association.GetAssociationType(protocol))) {
479 association = null;
482 if (association != null && !association.HasUsefulLifeRemaining) {
483 association = null;
486 return association;
489 /// <summary>
490 /// Gets an existing association with the specified Provider, or attempts to create
491 /// a new association of one does not already exist.
492 /// </summary>
493 /// <param name="provider">The provider to get an association for.</param>
494 /// <returns>The existing or new association; <c>null</c> if none existed and one could not be created.</returns>
495 internal Association GetOrCreateAssociation(ProviderEndpointDescription provider) {
496 return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider);
499 /// <summary>
500 /// Gets the priority rating for a given type of endpoint, allowing a
501 /// priority sorting of endpoints.
502 /// </summary>
503 /// <param name="endpoint">The endpoint to prioritize.</param>
504 /// <returns>An arbitary integer, which may be used for sorting against other returned values from this method.</returns>
505 private static double GetEndpointPrecedenceOrderByServiceType(IXrdsProviderEndpoint endpoint) {
506 // The numbers returned from this method only need to compare against other numbers
507 // from this method, which makes them arbitrary but relational to only others here.
508 if (endpoint.IsTypeUriPresent(Protocol.V20.OPIdentifierServiceTypeURI)) {
509 return 0;
511 if (endpoint.IsTypeUriPresent(Protocol.V20.ClaimedIdentifierServiceTypeURI)) {
512 return 1;
514 if (endpoint.IsTypeUriPresent(Protocol.V11.ClaimedIdentifierServiceTypeURI)) {
515 return 2;
517 if (endpoint.IsTypeUriPresent(Protocol.V10.ClaimedIdentifierServiceTypeURI)) {
518 return 3;
520 return 10;
523 /// <summary>
524 /// Creates a new association with a given Provider.
525 /// </summary>
526 /// <param name="provider">The provider to create an association with.</param>
527 /// <returns>
528 /// The newly created association, or null if no association can be created with
529 /// the given Provider given the current security settings.
530 /// </returns>
531 /// <remarks>
532 /// A new association is created and returned even if one already exists in the
533 /// association store.
534 /// Any new association is automatically added to the <see cref="AssociationStore"/>.
535 /// </remarks>
536 private Association CreateNewAssociation(ProviderEndpointDescription provider) {
537 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
539 // If there is no association store, there is no point in creating an association.
540 if (this.AssociationStore == null) {
541 return null;
544 var associateRequest = AssociateRequest.Create(this.SecuritySettings, provider);
545 if (associateRequest == null) {
546 // this can happen if security requirements and protocol conflict
547 // to where there are no association types to choose from.
548 return null;
551 var associateResponse = this.Channel.Request(associateRequest);
552 var associateSuccessfulResponse = associateResponse as AssociateSuccessfulResponse;
553 var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse;
554 if (associateSuccessfulResponse != null) {
555 Association association = associateSuccessfulResponse.CreateAssociation(associateRequest);
556 this.AssociationStore.StoreAssociation(provider.Endpoint, association);
557 return association;
558 } else if (associateUnsuccessfulResponse != null) {
559 // TODO: code here
560 throw new NotImplementedException();
561 } else {
562 throw new ProtocolException(MessagingStrings.UnexpectedMessageReceivedOfMany);