1
//-----------------------------------------------------------------------
2 // <copyright file="OpenIdRelyingParty.cs" company="Andrew Arnott">
3 // Copyright (c) Andrew Arnott. All rights reserved.
5 //-----------------------------------------------------------------------
7 namespace DotNetOpenAuth
.OpenId
.RelyingParty
{
9 using System
.Collections
.Generic
;
10 using System
.Collections
.Specialized
;
11 using System
.ComponentModel
;
14 using DotNetOpenAuth
.Configuration
;
15 using DotNetOpenAuth
.Messaging
;
16 using DotNetOpenAuth
.Messaging
.Bindings
;
17 using DotNetOpenAuth
.OpenId
.ChannelElements
;
18 using DotNetOpenAuth
.OpenId
.Messages
;
21 /// A delegate that decides whether a given OpenID Provider endpoint may be
22 /// considered for authenticating a user.
24 /// <param name="endpoint">The endpoint for consideration.</param>
26 /// <c>True</c> if the endpoint should be considered.
27 /// <c>False</c> to remove it from the pool of acceptable providers.
29 public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint
);
32 /// Provides the programmatic facilities to act as an OpenId consumer.
34 public sealed class OpenIdRelyingParty
{
36 /// Backing field for the <see cref="SecuritySettings"/> property.
38 private RelyingPartySecuritySettings securitySettings
;
41 /// Backing store for the <see cref="EndpointOrder"/> property.
43 private Comparison
<IXrdsProviderEndpoint
> endpointOrder
= DefaultEndpointOrder
;
46 /// Initializes a new instance of the <see cref="OpenIdRelyingParty"/> class.
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();
64 /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
65 /// attribute to determine order.
68 /// Endpoints lacking any priority value are sorted to the end of the list.
70 [EditorBrowsable(EditorBrowsableState
.Advanced
)]
71 public static Comparison
<IXrdsProviderEndpoint
> DefaultEndpointOrder
{
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
));
80 if (se1
.ServicePriority
.HasValue
&& se2
.ServicePriority
.HasValue
) {
81 result
= se1
.ServicePriority
.Value
.CompareTo(se2
.ServicePriority
.Value
);
85 if (se1
.UriPriority
.HasValue
&& se2
.UriPriority
.HasValue
) {
86 return se1
.UriPriority
.Value
.CompareTo(se2
.UriPriority
.Value
);
87 } else if (se1
.UriPriority
.HasValue
) {
89 } else if (se2
.UriPriority
.HasValue
) {
95 if (se1
.ServicePriority
.HasValue
) {
97 } else if (se2
.ServicePriority
.HasValue
) {
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
) {
105 } else if (se2
.UriPriority
.HasValue
) {
117 /// Gets the channel to use for sending/receiving messages.
119 public Channel Channel { get; internal set; }
122 /// Gets the security settings used by this Relying Party.
124 public RelyingPartySecuritySettings SecuritySettings
{
126 return this.securitySettings
;
131 throw new ArgumentNullException("value");
134 this.securitySettings
= value;
139 /// Gets or sets the optional Provider Endpoint filter to use.
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.
146 [EditorBrowsable(EditorBrowsableState
.Advanced
)]
147 public EndpointSelector EndpointFilter { get; set; }
150 /// Gets or sets the ordering routine that will determine which XRDS
151 /// Service element to try first
153 /// <value>Default is <see cref="DefaultEndpointOrder"/>.</value>
155 /// This may never be null. To reset to default behavior this property
156 /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
158 [EditorBrowsable(EditorBrowsableState
.Advanced
)]
159 public Comparison
<IXrdsProviderEndpoint
> EndpointOrder
{
161 return this.endpointOrder
;
165 ErrorUtilities
.VerifyArgumentNotNull(value, "value");
166 this.endpointOrder
= value;
171 /// Gets the association store.
173 internal IAssociationStore
<Uri
> AssociationStore { get; private set; }
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.
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; }
187 /// Creates an authentication request to verify that a user controls
188 /// some given Identifier.
190 /// <param name="userSuppliedIdentifier">
191 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
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/.
198 /// <param name="returnToUrl">
199 /// The URL of the login page, or the page prepared to receive authentication
200 /// responses from the OpenID Provider.
203 /// An authentication request object that describes the HTTP response to
204 /// send to the user agent to initiate the authentication.
206 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
207 public IAuthenticationRequest
CreateRequest(Identifier userSuppliedIdentifier
, Realm realm
, Uri returnToUrl
) {
209 return this.CreateRequests(userSuppliedIdentifier
, realm
, returnToUrl
).First();
210 } catch (InvalidOperationException ex
) {
211 throw ErrorUtilities
.Wrap(ex
, OpenIdStrings
.OpenIdEndpointNotFound
);
216 /// Creates an authentication request to verify that a user controls
217 /// some given Identifier.
219 /// <param name="userSuppliedIdentifier">
220 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
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/.
228 /// An authentication request object that describes the HTTP response to
229 /// send to the user agent to initiate the authentication.
232 /// This method requires an ASP.NET HttpContext.
234 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
235 public IAuthenticationRequest
CreateRequest(Identifier userSuppliedIdentifier
, Realm realm
) {
237 return this.CreateRequests(userSuppliedIdentifier
, realm
).First();
238 } catch (InvalidOperationException ex
) {
239 throw ErrorUtilities
.Wrap(ex
, OpenIdStrings
.OpenIdEndpointNotFound
);
244 /// Creates an authentication request to verify that a user controls
245 /// some given Identifier.
247 /// <param name="userSuppliedIdentifier">
248 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
251 /// An authentication request object that describes the HTTP response to
252 /// send to the user agent to initiate the authentication.
255 /// This method requires an ASP.NET HttpContext.
257 /// <exception cref="ProtocolException">Thrown if no OpenID endpoint could be found.</exception>
258 public IAuthenticationRequest
CreateRequest(Identifier userSuppliedIdentifier
) {
260 return this.CreateRequests(userSuppliedIdentifier
).First();
261 } catch (InvalidOperationException ex
) {
262 throw ErrorUtilities
.Wrap(ex
, OpenIdStrings
.OpenIdEndpointNotFound
);
267 /// Gets an authentication response from a Provider.
269 /// <returns>The processed authentication response if there is any; <c>null</c> otherwise.</returns>
271 /// This method requires an ASP.NET HttpContext.
273 public IAuthenticationResponse
GetResponse() {
274 return this.GetResponse(this.Channel
.GetRequestFromContext());
278 /// Gets an authentication response from a Provider.
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
) {
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
);
296 } catch (ProtocolException ex
) {
297 return new FailedAuthenticationResponse(ex
);
302 /// Determines whether some parameter name belongs to OpenID or this library
303 /// as a protocol or internal parameter name.
305 /// <param name="parameterName">Name of the parameter.</param>
307 /// <c>true</c> if the named parameter is a library- or protocol-specific parameter; otherwise, <c>false</c>.
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
);
316 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
318 /// <param name="userSuppliedIdentifier">
319 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
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/.
326 /// <param name="returnToUrl">
327 /// The URL of the login page, or the page prepared to receive authentication
328 /// responses from the OpenID Provider.
331 /// An authentication request object that describes the HTTP response to
332 /// send to the user agent to initiate the authentication.
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>
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
>();
359 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
361 /// <param name="userSuppliedIdentifier">
362 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
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/.
370 /// An authentication request object that describes the HTTP response to
371 /// send to the user agent to initiate the authentication.
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>
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
);
405 /// Generates the authentication requests that can satisfy the requirements of some OpenID Identifier.
407 /// <param name="userSuppliedIdentifier">
408 /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
411 /// An authentication request object that describes the HTTP response to
412 /// send to the user agent to initiate the authentication.
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>
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
));
445 /// Gets an association between this Relying Party and a given Provider
446 /// if it already exists in the association store.
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) {
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
))) {
470 if (association
!= null && !association
.HasUsefulLifeRemaining
) {
478 /// Gets an existing association with the specified Provider, or attempts to create
479 /// a new association of one does not already exist.
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
);
488 /// Gets the priority rating for a given type of endpoint, allowing a
489 /// priority sorting of endpoints.
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
)) {
499 if (endpoint
.IsTypeUriPresent(Protocol
.V20
.ClaimedIdentifierServiceTypeURI
)) {
502 if (endpoint
.IsTypeUriPresent(Protocol
.V11
.ClaimedIdentifierServiceTypeURI
)) {
505 if (endpoint
.IsTypeUriPresent(Protocol
.V10
.ClaimedIdentifierServiceTypeURI
)) {
512 /// Creates a new association with a given Provider.
514 /// <param name="provider">The provider to create an association with.</param>
516 /// The newly created association, or null if no association can be created with
517 /// the given Provider given the current security settings.
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"/>.
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.
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
);
541 } else if (associateUnsuccessfulResponse
!= null) {
543 throw new NotImplementedException();
545 throw new ProtocolException(MessagingStrings
.UnexpectedMessageReceivedOfMany
);