1
//-----------------------------------------------------------------------
2 // <copyright file="AuthenticationRequest.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
;
14 using DotNetOpenAuth
.Messaging
;
15 using DotNetOpenAuth
.OpenId
.ChannelElements
;
16 using DotNetOpenAuth
.OpenId
.Messages
;
19 /// Facilitates customization and creation and an authentication request
20 /// that a Relying Party is preparing to send.
22 internal class AuthenticationRequest
: IAuthenticationRequest
{
24 /// The name of the internal callback parameter to use to store the user-supplied identifier.
26 internal const string UserSuppliedIdentifierParameterName
= "dnoi.userSuppliedIdentifier";
29 /// The relying party that created this request object.
31 private readonly OpenIdRelyingParty RelyingParty
;
34 /// The endpoint that describes the particular OpenID Identifier and Provider that
35 /// will be used to create the authentication request.
37 private readonly ServiceEndpoint endpoint
;
40 /// The protocol version used at the Provider.
42 private readonly Protocol protocol
;
45 /// How an association may or should be created or used in the formulation of the
46 /// authentication request.
48 private AssociationPreference associationPreference
= AssociationPreference
.IfPossible
;
51 /// The extensions that have been added to this authentication request.
53 private List
<IOpenIdMessageExtension
> extensions
= new List
<IOpenIdMessageExtension
>();
56 /// Arguments to add to the return_to part of the query string, so that
57 /// these values come back to the consumer when the user agent returns.
59 private Dictionary
<string, string> returnToArgs
= new Dictionary
<string, string>();
62 /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class.
64 /// <param name="endpoint">The endpoint that describes the OpenID Identifier and Provider that will complete the authentication.</param>
65 /// <param name="realm">The realm, or root URL, of the host web site.</param>
66 /// <param name="returnToUrl">The base return_to URL that the Provider should return the user to to complete authentication. This should not include callback parameters as these should be added using the <see cref="AddCallbackArguments(string, string)"/> method.</param>
67 /// <param name="relyingParty">The relying party that created this instance.</param>
68 private AuthenticationRequest(ServiceEndpoint endpoint
, Realm realm
, Uri returnToUrl
, OpenIdRelyingParty relyingParty
) {
69 ErrorUtilities
.VerifyArgumentNotNull(endpoint
, "endpoint");
70 ErrorUtilities
.VerifyArgumentNotNull(realm
, "realm");
71 ErrorUtilities
.VerifyArgumentNotNull(returnToUrl
, "returnToUrl");
72 ErrorUtilities
.VerifyArgumentNotNull(relyingParty
, "relyingParty");
74 this.endpoint
= endpoint
;
75 this.protocol
= endpoint
.Protocol
;
76 this.RelyingParty
= relyingParty
;
78 this.ReturnToUrl
= returnToUrl
;
80 this.Mode
= AuthenticationRequestMode
.Setup
;
83 #region IAuthenticationRequest Members
86 /// Gets or sets the mode the Provider should use during authentication.
89 public AuthenticationRequestMode Mode { get; set; }
92 /// Gets the HTTP response the relying party should send to the user agent
93 /// to redirect it to the OpenID Provider to start the OpenID authentication process.
96 public UserAgentResponse RedirectingResponse
{
97 get { return this.RelyingParty.Channel.PrepareResponse(this.CreateRequestMessage()); }
101 /// Gets the URL that the user agent will return to after authentication
102 /// completes or fails at the Provider.
105 public Uri ReturnToUrl { get; private set; }
108 /// Gets the URL that identifies this consumer web application that
109 /// the Provider will display to the end user.
111 public Realm Realm { get; private set; }
114 /// Gets the Claimed Identifier that the User Supplied Identifier
115 /// resolved to. Null if the user provided an OP Identifier
116 /// (directed identity).
120 /// Null is returned if the user is using the directed identity feature
121 /// of OpenID 2.0 to make it nearly impossible for a relying party site
122 /// to improperly store the reserved OpenID URL used for directed identity
123 /// as a user's own Identifier.
124 /// However, to test for the Directed Identity feature, please test the
125 /// <see cref="IsDirectedIdentity"/> property rather than testing this
126 /// property for a null value.
128 public Identifier ClaimedIdentifier
{
129 get { return this.IsDirectedIdentity ? null : this.endpoint.ClaimedIdentifier; }
133 /// Gets a value indicating whether the authenticating user has chosen to let the Provider
134 /// determine and send the ClaimedIdentifier after authentication.
137 public bool IsDirectedIdentity
{
138 get { return this.endpoint.ClaimedIdentifier == this.endpoint.Protocol.ClaimedIdentifierForOPIdentifier; }
142 /// Gets information about the OpenId Provider, as advertised by the
143 /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
147 IProviderEndpoint IAuthenticationRequest
.Provider
{
148 get { return this.endpoint; }
152 /// Gets the detected version of OpenID implemented by the Provider.
155 public Version ProviderVersion
{
156 get { return this.endpoint.Protocol.Version; }
162 /// Gets or sets how an association may or should be created or used
163 /// in the formulation of the authentication request.
165 internal AssociationPreference AssociationPreference
{
166 get { return this.associationPreference; }
167 set { this.associationPreference = value; }
170 #region IAuthenticationRequest methods
173 /// Makes a dictionary of key/value pairs available when the authentication is completed.
175 /// <param name="arguments">The arguments to add to the request's return_to URI.</param>
177 /// <para>Note that these values are NOT protected against tampering in transit. No
178 /// security-sensitive data should be stored using this method.</para>
179 /// <para>The values stored here can be retrieved using
180 /// <see cref="IAuthenticationResponse.GetCallbackArguments"/>.</para>
181 /// <para>Since the data set here is sent in the querystring of the request and some
182 /// servers place limits on the size of a request URL, this data should be kept relatively
183 /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
185 public void AddCallbackArguments(IDictionary
<string, string> arguments
) {
186 ErrorUtilities
.VerifyArgumentNotNull(arguments
, "arguments");
187 ErrorUtilities
.VerifyOperation(this.RelyingParty
.CanSignCallbackArguments
, typeof(IPrivateSecretStore
).Name
, typeof(OpenIdRelyingParty
).Name
);
189 foreach (var pair
in arguments
) {
190 ErrorUtilities
.VerifyArgument(!string.IsNullOrEmpty(pair
.Key
), MessagingStrings
.UnexpectedNullOrEmptyKey
);
191 ErrorUtilities
.VerifyArgument(pair
.Value
!= null, MessagingStrings
.UnexpectedNullValue
, pair
.Key
);
193 this.returnToArgs
.Add(pair
.Key
, pair
.Value
);
198 /// Makes a key/value pair available when the authentication is completed.
200 /// <param name="key">The parameter name.</param>
201 /// <param name="value">The value of the argument.</param>
203 /// <para>Note that these values are NOT protected against tampering in transit. No
204 /// security-sensitive data should be stored using this method.</para>
205 /// <para>The value stored here can be retrieved using
206 /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
207 /// <para>Since the data set here is sent in the querystring of the request and some
208 /// servers place limits on the size of a request URL, this data should be kept relatively
209 /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
211 public void AddCallbackArguments(string key
, string value) {
212 ErrorUtilities
.VerifyNonZeroLength(key
, "key");
213 ErrorUtilities
.VerifyArgumentNotNull(value, "value");
214 ErrorUtilities
.VerifyOperation(this.RelyingParty
.CanSignCallbackArguments
, typeof(IPrivateSecretStore
).Name
, typeof(OpenIdRelyingParty
).Name
);
216 this.returnToArgs
.Add(key
, value);
220 /// Adds an OpenID extension to the request directed at the OpenID provider.
222 /// <param name="extension">The initialized extension to add to the request.</param>
223 public void AddExtension(IOpenIdMessageExtension extension
) {
224 ErrorUtilities
.VerifyArgumentNotNull(extension
, "extension");
225 this.extensions
.Add(extension
);
229 /// Redirects the user agent to the provider for authentication.
230 /// Execution of the current page terminates after this call.
233 /// This method requires an ASP.NET HttpContext.
235 public void RedirectToProvider() {
236 this.RedirectingResponse
.Send();
242 /// Performs identifier discovery, creates associations and generates authentication requests
243 /// on-demand for as long as new ones can be generated based on the results of Identifier discovery.
245 /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
246 /// <param name="relyingParty">The relying party.</param>
247 /// <param name="realm">The realm.</param>
248 /// <param name="returnToUrl">The return_to base URL.</param>
249 /// <param name="createNewAssociationsAsNeeded">if set to <c>true</c>, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created.</param>
250 /// <returns>A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.</returns>
251 internal static IEnumerable
<AuthenticationRequest
> Create(Identifier userSuppliedIdentifier
, OpenIdRelyingParty relyingParty
, Realm realm
, Uri returnToUrl
, bool createNewAssociationsAsNeeded
) {
252 // We have a long data validation and preparation process
253 ErrorUtilities
.VerifyArgumentNotNull(userSuppliedIdentifier
, "userSuppliedIdentifier");
254 ErrorUtilities
.VerifyArgumentNotNull(relyingParty
, "relyingParty");
255 ErrorUtilities
.VerifyArgumentNotNull(realm
, "realm");
257 // Normalize the portion of the return_to path that correlates to the realm for capitalization.
258 // (so that if a web app base path is /MyApp/, but the URL of this request happens to be
259 // /myapp/login.aspx, we bump up the return_to Url to use /MyApp/ so it matches the realm.
260 UriBuilder returnTo
= new UriBuilder(returnToUrl
);
261 if (returnTo
.Path
.StartsWith(realm
.AbsolutePath
, StringComparison
.OrdinalIgnoreCase
) &&
262 !returnTo
.Path
.StartsWith(realm
.AbsolutePath
, StringComparison
.Ordinal
)) {
263 returnTo
.Path
= realm
.AbsolutePath
+ returnTo
.Path
.Substring(realm
.AbsolutePath
.Length
);
264 returnToUrl
= returnTo
.Uri
;
267 userSuppliedIdentifier
= userSuppliedIdentifier
.TrimFragment();
268 if (relyingParty
.SecuritySettings
.RequireSsl
) {
269 // Rather than check for successful SSL conversion at this stage,
270 // We'll wait for secure discovery to fail on the new identifier.
271 userSuppliedIdentifier
.TryRequireSsl(out userSuppliedIdentifier
);
274 if (Logger
.IsWarnEnabled
&& returnToUrl
.Query
!= null) {
275 NameValueCollection returnToArgs
= HttpUtility
.ParseQueryString(returnToUrl
.Query
);
276 foreach (string key
in returnToArgs
) {
277 if (OpenIdRelyingParty
.IsOpenIdSupportingParameter(key
)) {
278 Logger
.WarnFormat("OpenID argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key
);
283 // Throw an exception now if the realm and the return_to URLs don't match
284 // as required by the provider. We could wait for the provider to test this and
285 // fail, but this will be faster and give us a better error message.
286 ErrorUtilities
.VerifyProtocol(realm
.Contains(returnToUrl
), OpenIdStrings
.ReturnToNotUnderRealm
, returnToUrl
, realm
);
288 // Perform discovery right now (not deferred).
289 var serviceEndpoints
= userSuppliedIdentifier
.Discover(relyingParty
.WebRequestHandler
);
291 // Call another method that defers request generation.
292 return CreateInternal(userSuppliedIdentifier
, relyingParty
, realm
, returnToUrl
, serviceEndpoints
, createNewAssociationsAsNeeded
);
296 /// Performs deferred request generation for the <see cref="Create"/> method.
298 /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
299 /// <param name="relyingParty">The relying party.</param>
300 /// <param name="realm">The realm.</param>
301 /// <param name="returnToUrl">The return_to base URL.</param>
302 /// <param name="serviceEndpoints">The discovered service endpoints on the Claimed Identifier.</param>
303 /// <param name="createNewAssociationsAsNeeded">if set to <c>true</c>, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created.</param>
305 /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
308 /// All data validation and cleansing steps must have ALREADY taken place
309 /// before calling this method.
311 private static IEnumerable
<AuthenticationRequest
> CreateInternal(Identifier userSuppliedIdentifier
, OpenIdRelyingParty relyingParty
, Realm realm
, Uri returnToUrl
, IEnumerable
<ServiceEndpoint
> serviceEndpoints
, bool createNewAssociationsAsNeeded
) {
312 Logger
.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier
);
313 IEnumerable
<ServiceEndpoint
> endpoints
= FilterAndSortEndpoints(serviceEndpoints
, relyingParty
);
315 // Maintain a list of endpoints that we could not form an association with.
316 // We'll fallback to generating requests to these if the ones we CAN create
317 // an association with run out.
318 var failedAssociationEndpoints
= new List
<ServiceEndpoint
>(0);
320 foreach (var endpoint
in endpoints
) {
321 Logger
.InfoFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier
);
322 Logger
.DebugFormat("Realm: {0}", realm
);
323 Logger
.DebugFormat("Return To: {0}", returnToUrl
);
325 // The strategy here is to prefer endpoints with whom we can create associations.
326 Association association
= null;
327 if (relyingParty
.AssociationManager
.HasAssociationStore
) {
328 // In some scenarios (like the AJAX control wanting ALL auth requests possible),
329 // we don't want to create associations with every Provider. But we'll use
330 // associations where they are already formed from previous authentications.
331 association
= createNewAssociationsAsNeeded
? relyingParty
.AssociationManager
.GetOrCreateAssociation(endpoint
.ProviderDescription
) : relyingParty
.AssociationManager
.GetExistingAssociation(endpoint
.ProviderDescription
);
332 if (association
== null && createNewAssociationsAsNeeded
) {
333 Logger
.WarnFormat("Failed to create association with {0}. Skipping to next endpoint.", endpoint
.ProviderEndpoint
);
335 // No association could be created. Add it to the list of failed association
336 // endpoints and skip to the next available endpoint.
337 failedAssociationEndpoints
.Add(endpoint
);
342 yield return new AuthenticationRequest(endpoint
, realm
, returnToUrl
, relyingParty
);
345 // Now that we've run out of endpoints that respond to association requests,
346 // since we apparently are still running, the caller must want another request.
347 // We'll go ahead and generate the requests to OPs that may be down.
348 if (failedAssociationEndpoints
.Count
> 0) {
349 Logger
.WarnFormat("Now generating requests for Provider endpoints that failed initial association attempts.");
351 foreach (var endpoint
in failedAssociationEndpoints
) {
352 Logger
.WarnFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier
);
353 Logger
.DebugFormat("Realm: {0}", realm
);
354 Logger
.DebugFormat("Return To: {0}", returnToUrl
);
356 // Create the auth request, but prevent it from attempting to create an association
357 // because we've already tried. Let's not have it waste time trying again.
358 var authRequest
= new AuthenticationRequest(endpoint
, realm
, returnToUrl
, relyingParty
);
359 authRequest
.associationPreference
= AssociationPreference
.IfAlreadyEstablished
;
360 yield return authRequest
;
366 /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
368 /// <param name="endpoints">The endpoints.</param>
369 /// <param name="relyingParty">The relying party.</param>
370 /// <returns>A filtered and sorted list of endpoints; may be empty if the input was empty or the filter removed all endpoints.</returns>
371 private static List
<ServiceEndpoint
> FilterAndSortEndpoints(IEnumerable
<ServiceEndpoint
> endpoints
, OpenIdRelyingParty relyingParty
) {
372 ErrorUtilities
.VerifyArgumentNotNull(endpoints
, "endpoints");
373 ErrorUtilities
.VerifyArgumentNotNull(relyingParty
, "relyingParty");
375 // Construct the endpoints filters based on criteria given by the host web site.
376 EndpointSelector versionFilter
= ep
=> ((ServiceEndpoint
)ep
).Protocol
.Version
>= Protocol
.Lookup(relyingParty
.SecuritySettings
.MinimumRequiredOpenIdVersion
).Version
;
377 EndpointSelector hostingSiteFilter
= relyingParty
.EndpointFilter
?? (ep
=> true);
379 bool anyFilteredOut
= false;
380 var filteredEndpoints
= new List
<IXrdsProviderEndpoint
>();
381 foreach (ServiceEndpoint endpoint
in endpoints
) {
382 if (versionFilter(endpoint
) && hostingSiteFilter(endpoint
)) {
383 filteredEndpoints
.Add(endpoint
);
385 anyFilteredOut
= true;
389 // Sort endpoints so that the first one in the list is the most preferred one.
390 filteredEndpoints
.Sort(relyingParty
.EndpointOrder
);
392 List
<ServiceEndpoint
> endpointList
= new List
<ServiceEndpoint
>(filteredEndpoints
.Count
);
393 foreach (ServiceEndpoint endpoint
in filteredEndpoints
) {
394 endpointList
.Add(endpoint
);
397 if (anyFilteredOut
) {
398 Logger
.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints
.Count
);
400 if (Logger
.IsDebugEnabled
) {
401 if (MessagingUtilities
.AreEquivalent(endpoints
, endpointList
)) {
402 Logger
.Debug("Filtering and sorting of endpoints did not affect the list.");
404 Logger
.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
405 Logger
.Debug(Util
.ToStringDeferred(filteredEndpoints
, true));
413 /// Creates the authentication request message to send to the Provider,
414 /// based on the properties in this instance.
416 /// <returns>The message to send to the Provider.</returns>
417 private CheckIdRequest
CreateRequestMessage() {
418 Association association
= this.GetAssociation();
420 CheckIdRequest request
= new CheckIdRequest(this.ProviderVersion
, this.endpoint
.ProviderEndpoint
, this.Mode
);
421 request
.ClaimedIdentifier
= this.endpoint
.ClaimedIdentifier
;
422 request
.LocalIdentifier
= this.endpoint
.ProviderLocalIdentifier
;
423 request
.Realm
= this.Realm
;
424 request
.ReturnTo
= this.ReturnToUrl
;
425 request
.AssociationHandle
= association
!= null ? association
.Handle
: null;
426 request
.AddReturnToArguments(this.returnToArgs
);
427 request
.AddReturnToArguments(UserSuppliedIdentifierParameterName
, this.endpoint
.UserSuppliedIdentifier
);
428 foreach (IOpenIdMessageExtension extension
in this.extensions
) {
429 request
.Extensions
.Add(extension
);
436 /// Gets the association to use for this authentication request.
438 /// <returns>The association to use; <c>null</c> to use 'dumb mode'.</returns>
439 private Association
GetAssociation() {
440 Association association
= null;
441 switch (this.associationPreference
) {
442 case AssociationPreference
.IfPossible
:
443 association
= this.RelyingParty
.AssociationManager
.GetOrCreateAssociation(this.endpoint
.ProviderDescription
);
444 if (association
== null) {
445 // Avoid trying to create the association again if the redirecting response
446 // is generated again.
447 this.associationPreference
= AssociationPreference
.IfAlreadyEstablished
;
450 case AssociationPreference
.IfAlreadyEstablished
:
451 association
= this.RelyingParty
.AssociationManager
.GetExistingAssociation(this.endpoint
.ProviderDescription
);
453 case AssociationPreference
.Never
:
456 throw new InternalErrorException();