StyleCop work
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / RelyingParty / OpenIdRelyingParty.cs
blob32bcbdcb13bd488db92541cfa6f0023b5d31380a
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="associationStore">The association store. If null, the relying party will always operate in "dumb mode".</param>
49 /// <param name="nonceStore">The nonce store to use. If null, the relying party will always operate in "dumb mode".</param>
50 /// <param name="secretStore">The secret store to use. If null, the relying party will always operate in "dumb mode".</param>
51 public OpenIdRelyingParty(IAssociationStore<Uri> associationStore, INonceStore nonceStore, IPrivateSecretStore secretStore) {
52 // TODO: fix this so that a null association store is supported as 'dumb mode only'.
53 ErrorUtilities.VerifyArgumentNotNull(associationStore, "associationStore");
54 ErrorUtilities.VerifyArgumentNotNull(nonceStore, "nonceStore");
55 ErrorUtilities.VerifyArgumentNotNull(secretStore, "secretStore");
56 ErrorUtilities.VerifyArgument((associationStore == null) == (nonceStore == null), OpenIdStrings.AssociationAndNonceStoresMustBeBothNullOrBothNonNull);
58 this.Channel = new OpenIdChannel(associationStore, nonceStore, secretStore);
59 this.AssociationStore = associationStore;
60 this.SecuritySettings = RelyingPartySection.Configuration.SecuritySettings.CreateSecuritySettings();
63 /// <summary>
64 /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
65 /// attribute to determine order.
66 /// </summary>
67 /// <remarks>
68 /// Endpoints lacking any priority value are sorted to the end of the list.
69 /// </remarks>
70 [EditorBrowsable(EditorBrowsableState.Advanced)]
71 public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
72 get {
73 // Sort first by service type (OpenID 2.0, 1.1, 1.0),
74 // then by Service/@priority, then by Service/Uri/@priority
75 return (se1, se2) => {
76 int result = GetEndpointPrecedenceOrderByServiceType(se1).CompareTo(GetEndpointPrecedenceOrderByServiceType(se2));
77 if (result != 0) {
78 return result;
80 if (se1.ServicePriority.HasValue && se2.ServicePriority.HasValue) {
81 result = se1.ServicePriority.Value.CompareTo(se2.ServicePriority.Value);
82 if (result != 0) {
83 return result;
85 if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) {
86 return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value);
87 } else if (se1.UriPriority.HasValue) {
88 return -1;
89 } else if (se2.UriPriority.HasValue) {
90 return 1;
91 } else {
92 return 0;
94 } else {
95 if (se1.ServicePriority.HasValue) {
96 return -1;
97 } else if (se2.ServicePriority.HasValue) {
98 return 1;
99 } else {
100 // neither service defines a priority, so base ordering by uri priority.
101 if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) {
102 return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value);
103 } else if (se1.UriPriority.HasValue) {
104 return -1;
105 } else if (se2.UriPriority.HasValue) {
106 return 1;
107 } else {
108 return 0;
116 /// <summary>
117 /// Gets the channel to use for sending/receiving messages.
118 /// </summary>
119 public Channel Channel { get; internal set; }
121 /// <summary>
122 /// Gets the security settings used by this Relying Party.
123 /// </summary>
124 public RelyingPartySecuritySettings SecuritySettings {
125 get {
126 return this.securitySettings;
129 internal set {
130 if (value == null) {
131 throw new ArgumentNullException("value");
134 this.securitySettings = value;
138 /// <summary>
139 /// Gets or sets the optional Provider Endpoint filter to use.
140 /// </summary>
141 /// <remarks>
142 /// Provides a way to optionally filter the providers that may be used in authenticating a user.
143 /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
144 /// If null, all identity providers will be accepted. This is the default.
145 /// </remarks>
146 [EditorBrowsable(EditorBrowsableState.Advanced)]
147 public EndpointSelector EndpointFilter { get; set; }
149 /// <summary>
150 /// Gets or sets the ordering routine that will determine which XRDS
151 /// Service element to try first
152 /// </summary>
153 /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
154 /// <remarks>
155 /// This may never be null. To reset to default behavior this property
156 /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
157 /// </remarks>
158 [EditorBrowsable(EditorBrowsableState.Advanced)]
159 public Comparison<IXrdsProviderEndpoint> EndpointOrder {
160 get {
161 return this.endpointOrder;
164 set {
165 ErrorUtilities.VerifyArgumentNotNull(value, "value");
166 this.endpointOrder = value;
170 /// <summary>
171 /// Gets the association store.
172 /// </summary>
173 internal IAssociationStore<Uri> AssociationStore { get; private set; }
175 /// <summary>
176 /// Gets the web request handler to use for discovery and the part of
177 /// authentication where direct messages are sent to an untrusted remote party.
178 /// </summary>
179 internal IDirectSslWebRequestHandler WebRequestHandler {
180 // TODO: Since the OpenIdChannel.WebRequestHandler might be set to a non-SSL
181 // implementation, we should consider altering the consumers of this property
182 // to handle either case.
183 get { return this.Channel.WebRequestHandler as IDirectSslWebRequestHandler; }
186 /// <summary>
187 /// Creates an authentication request to verify that a user controls
188 /// some given Identifier.
189 /// </summary>
190 /// <param name="userSuppliedIdentifier">
191 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
192 /// </param>
193 /// <param name="realm">
194 /// The shorest URL that describes this relying party web site's address.
195 /// For example, if your login page is found at https://www.example.com/login.aspx,
196 /// your realm would typically be https://www.example.com/.
197 /// </param>
198 /// <param name="returnToUrl">
199 /// The URL of the login page, or the page prepared to receive authentication
200 /// responses from the OpenID Provider.
201 /// </param>
202 /// <returns>
203 /// An authentication request object that describes the HTTP response to
204 /// send to the user agent to initiate the authentication.
205 /// </returns>
206 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
207 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
208 try {
209 return this.CreateRequests(userSuppliedIdentifier, realm, returnToUrl).First();
210 } catch (InvalidOperationException ex) {
211 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
215 /// <summary>
216 /// Creates an authentication request to verify that a user controls
217 /// some given Identifier.
218 /// </summary>
219 /// <param name="userSuppliedIdentifier">
220 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
221 /// </param>
222 /// <param name="realm">
223 /// The shorest URL that describes this relying party web site's address.
224 /// For example, if your login page is found at https://www.example.com/login.aspx,
225 /// your realm would typically be https://www.example.com/.
226 /// </param>
227 /// <returns>
228 /// An authentication request object that describes the HTTP response to
229 /// send to the user agent to initiate the authentication.
230 /// </returns>
231 /// <remarks>
232 /// This method requires an ASP.NET HttpContext.
233 /// </remarks>
234 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
235 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm) {
236 try {
237 return this.CreateRequests(userSuppliedIdentifier, realm).First();
238 } catch (InvalidOperationException ex) {
239 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
243 /// <summary>
244 /// Creates an authentication request to verify that a user controls
245 /// some given Identifier.
246 /// </summary>
247 /// <param name="userSuppliedIdentifier">
248 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
249 /// </param>
250 /// <returns>
251 /// An authentication request object that describes the HTTP response to
252 /// send to the user agent to initiate the authentication.
253 /// </returns>
254 /// <remarks>
255 /// This method requires an ASP.NET HttpContext.
256 /// </remarks>
257 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
258 public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier) {
259 try {
260 return this.CreateRequests(userSuppliedIdentifier).First();
261 } catch (InvalidOperationException ex) {
262 throw ErrorUtilities.Wrap(ex, OpenIdStrings.OpenIdEndpointNotFound);
266 /// <summary>
267 /// Gets an authentication response from a Provider.
268 /// </summary>
269 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
270 /// <remarks>
271 /// This method requires an ASP.NET HttpContext.
272 /// </remarks>
273 public IAuthenticationResponse GetResponse() {
274 return this.GetResponse(this.Channel.GetRequestFromContext());
277 /// <summary>
278 /// Gets an authentication response from a Provider.
279 /// </summary>
280 /// <param name="httpRequestInfo">The HTTP request that may be carrying an authentication response from the Provider.</param>
281 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
282 public IAuthenticationResponse GetResponse(HttpRequestInfo httpRequestInfo) {
283 try {
284 var message = this.Channel.ReadFromRequest();
285 PositiveAssertionResponse positiveAssertion;
286 NegativeAssertionResponse negativeAssertion;
287 if ((positiveAssertion = message as PositiveAssertionResponse) != null) {
288 return new PositiveAuthenticationResponse(positiveAssertion, this);
289 } else if ((negativeAssertion = message as NegativeAssertionResponse) != null) {
290 return new NegativeAuthenticationResponse(negativeAssertion);
291 } else if (message != null) {
292 Logger.WarnFormat("Received unexpected message type {0} when expecting an assertion message.", message.GetType().Name);
295 return null;
296 } catch (ProtocolException ex) {
297 return new FailedAuthenticationResponse(ex);
301 /// <summary>
302 /// Determines whether some parameter name belongs to OpenID or this library
303 /// as a protocol or internal parameter name.
304 /// </summary>
305 /// <param name="parameterName">Name of the parameter.</param>
306 /// <returns>
307 /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
308 /// </returns>
309 internal static bool IsOpenIdSupportingParameter(string parameterName) {
310 Protocol protocol = Protocol.Default;
311 return parameterName.StartsWith(protocol.openid.Prefix, StringComparison.OrdinalIgnoreCase)
312 || parameterName.StartsWith("dnoi.", StringComparison.Ordinal);
315 /// <summary>
316 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
317 /// </summary>
318 /// <param name="userSuppliedIdentifier">
319 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
320 /// </param>
321 /// <param name="realm">
322 /// The shorest URL that describes this relying party web site's address.
323 /// For example, if your login page is found at https://www.example.com/login.aspx,
324 /// your realm would typically be https://www.example.com/.
325 /// </param>
326 /// <param name="returnToUrl">
327 /// The URL of the login page, or the page prepared to receive authentication
328 /// responses from the OpenID Provider.
329 /// </param>
330 /// <returns>
331 /// An authentication request object that describes the HTTP response to
332 /// send to the user agent to initiate the authentication.
333 /// </returns>
334 /// <remarks>
335 /// <para>Any individual generated request can satisfy the authentication.
336 /// The generated requests are sorted in preferred order.
337 /// Each request is generated as it is enumerated to. Associations are created only as
338 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
339 /// <para>No exception is thrown if no OpenID endpoints were discovered.
340 /// An empty enumerable is returned instead.</para>
341 /// </remarks>
342 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
343 ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
344 ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl");
346 // Normalize the portion of the return_to path that correlates to the realm for capitalization.
347 // (so that if a web app base path is /MyApp/, but the URL of this request happens to be
348 // /myapp/login.aspx, we bump up the return_to Url to use /MyApp/ so it matches the realm.
349 UriBuilder returnTo = new UriBuilder(returnToUrl);
350 if (returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.OrdinalIgnoreCase) &&
351 !returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.Ordinal)) {
352 returnTo.Path = realm.AbsolutePath + returnTo.Path.Substring(realm.AbsolutePath.Length);
355 return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnTo.Uri, true).Cast<IAuthenticationRequest>();
358 /// <summary>
359 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
360 /// </summary>
361 /// <param name="userSuppliedIdentifier">
362 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
363 /// </param>
364 /// <param name="realm">
365 /// The shorest URL that describes this relying party web site's address.
366 /// For example, if your login page is found at https://www.example.com/login.aspx,
367 /// your realm would typically be https://www.example.com/.
368 /// </param>
369 /// <returns>
370 /// An authentication request object that describes the HTTP response to
371 /// send to the user agent to initiate the authentication.
372 /// </returns>
373 /// <remarks>
374 /// <para>Any individual generated request can satisfy the authentication.
375 /// The generated requests are sorted in preferred order.
376 /// Each request is generated as it is enumerated to. Associations are created only as
377 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
378 /// <para>No exception is thrown if no OpenID endpoints were discovered.
379 /// An empty enumerable is returned instead.</para>
380 /// </remarks>
381 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier, Realm realm) {
382 if (HttpContext.Current == null) {
383 throw new InvalidOperationException(MessagingStrings.HttpContextRequired);
386 // Build the return_to URL
387 UriBuilder returnTo = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
389 // Trim off any parameters with an "openid." prefix, and a few known others
390 // to avoid carrying state from a prior login attempt.
391 returnTo.Query = string.Empty;
392 NameValueCollection queryParams = MessagingUtilities.GetQueryFromContextNVC();
393 var returnToParams = new Dictionary<string, string>(queryParams.Count);
394 foreach (string key in queryParams) {
395 if (!IsOpenIdSupportingParameter(key)) {
396 returnToParams.Add(key, queryParams[key]);
399 returnTo.AppendQueryArgs(returnToParams);
401 return this.CreateRequests(userSuppliedIdentifier, realm, returnTo.Uri);
404 /// <summary>
405 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
406 /// </summary>
407 /// <param name="userSuppliedIdentifier">
408 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
409 /// </param>
410 /// <returns>
411 /// An authentication request object that describes the HTTP response to
412 /// send to the user agent to initiate the authentication.
413 /// </returns>
414 /// <remarks>
415 /// <para>Any individual generated request can satisfy the authentication.
416 /// The generated requests are sorted in preferred order.
417 /// Each request is generated as it is enumerated to. Associations are created only as
418 /// <see cref="IAuthenticationRequest.RedirectingResponse"/> is called.</para>
419 /// <para>No exception is thrown if no OpenID endpoints were discovered.
420 /// An empty enumerable is returned instead.</para>
421 /// </remarks>
422 internal IEnumerable<IAuthenticationRequest> CreateRequests(Identifier userSuppliedIdentifier) {
423 if (HttpContext.Current == null) {
424 throw new InvalidOperationException(MessagingStrings.HttpContextRequired);
427 // Build the realm URL
428 UriBuilder realmUrl = new UriBuilder(MessagingUtilities.GetRequestUrlFromContext());
429 realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
430 realmUrl.Query = null;
431 realmUrl.Fragment = null;
433 // For RP discovery, the realm url MUST NOT redirect. To prevent this for
434 // virtual directory hosted apps, we need to make sure that the realm path ends
435 // in a slash (since our calculation above guarantees it doesn't end in a specific
436 // page like default.aspx).
437 if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal)) {
438 realmUrl.Path += "/";
441 return this.CreateRequests(userSuppliedIdentifier, new Realm(realmUrl.Uri));
444 /// <summary>
445 /// Gets an association between this Relying Party and a given Provider
446 /// if it already exists in the association store.
447 /// </summary>
448 /// <param name="provider">The provider to create an association with.</param>
449 /// <returns>The association if one exists and has useful life remaining. Otherwise <c>null</c>.</returns>
450 internal Association GetExistingAssociation(ProviderEndpointDescription provider) {
451 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
453 Protocol protocol = Protocol.Lookup(provider.ProtocolVersion);
455 // If the RP has no application store for associations, there's no point in creating one.
456 if (this.AssociationStore == null) {
457 return null;
460 // TODO: we need a way to lookup an association that fulfills a given set of security
461 // requirements. We may have a SHA-1 association and a SHA-256 association that need
462 // to be called for specifically. (a bizzare scenario, admittedly, making this low priority).
463 Association association = this.AssociationStore.GetAssociation(provider.Endpoint);
465 // If the returned association does not fulfill security requirements, ignore it.
466 if (association != null && !this.SecuritySettings.IsAssociationInPermittedRange(protocol, association.GetAssociationType(protocol))) {
467 association = null;
470 if (association != null && !association.HasUsefulLifeRemaining) {
471 association = null;
474 return association;
477 /// <summary>
478 /// Gets an existing association with the specified Provider, or attempts to create
479 /// a new association of one does not already exist.
480 /// </summary>
481 /// <param name="provider">The provider to get an association for.</param>
482 /// <returns>The existing or new association; <c>null</c> if none existed and one could not be created.</returns>
483 internal Association GetOrCreateAssociation(ProviderEndpointDescription provider) {
484 return this.GetExistingAssociation(provider) ?? this.CreateNewAssociation(provider);
487 /// <summary>
488 /// Gets the priority rating for a given type of endpoint, allowing a
489 /// priority sorting of endpoints.
490 /// </summary>
491 /// <param name="endpoint">The endpoint to prioritize.</param>
492 /// <returns>An arbitary integer, which may be used for sorting against other returned values from this method.</returns>
493 private static double GetEndpointPrecedenceOrderByServiceType(IXrdsProviderEndpoint endpoint) {
494 // The numbers returned from this method only need to compare against other numbers
495 // from this method, which makes them arbitrary but relational to only others here.
496 if (endpoint.IsTypeUriPresent(Protocol.V20.OPIdentifierServiceTypeURI)) {
497 return 0;
499 if (endpoint.IsTypeUriPresent(Protocol.V20.ClaimedIdentifierServiceTypeURI)) {
500 return 1;
502 if (endpoint.IsTypeUriPresent(Protocol.V11.ClaimedIdentifierServiceTypeURI)) {
503 return 2;
505 if (endpoint.IsTypeUriPresent(Protocol.V10.ClaimedIdentifierServiceTypeURI)) {
506 return 3;
508 return 10;
511 /// <summary>
512 /// Creates a new association with a given Provider.
513 /// </summary>
514 /// <param name="provider">The provider to create an association with.</param>
515 /// <returns>
516 /// The newly created association, or null if no association can be created with
517 /// the given Provider given the current security settings.
518 /// </returns>
519 /// <remarks>
520 /// A new association is created and returned even if one already exists in the
521 /// association store.
522 /// Any new association is automatically added to the <see cref="AssociationStore"/>.
523 /// </remarks>
524 private Association CreateNewAssociation(ProviderEndpointDescription provider) {
525 ErrorUtilities.VerifyArgumentNotNull(provider, "provider");
527 var associateRequest = AssociateRequest.Create(this.SecuritySettings, provider);
528 if (associateRequest == null) {
529 // this can happen if security requirements and protocol conflict
530 // to where there are no association types to choose from.
531 return null;
534 var associateResponse = this.Channel.Request(associateRequest);
535 var associateSuccessfulResponse = associateResponse as AssociateSuccessfulResponse;
536 var associateUnsuccessfulResponse = associateResponse as AssociateUnsuccessfulResponse;
537 if (associateSuccessfulResponse != null) {
538 Association association = associateSuccessfulResponse.CreateAssociation(associateRequest);
539 this.AssociationStore.StoreAssociation(provider.Endpoint, association);
540 return association;
541 } else if (associateUnsuccessfulResponse != null) {
542 // TODO: code here
543 throw new NotImplementedException();
544 } else {
545 throw new ProtocolException(MessagingStrings.UnexpectedMessageReceivedOfMany);