Added the OpenIdAjaxTextBox control.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / RelyingParty / AuthenticationRequest.cs
blobf76a5a43569762140252e9384be525298f549b11
1 //-----------------------------------------------------------------------
2 // <copyright file="AuthenticationRequest.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.Linq;
12 using System.Text;
13 using System.Web;
14 using DotNetOpenAuth.Messaging;
15 using DotNetOpenAuth.OpenId.ChannelElements;
16 using DotNetOpenAuth.OpenId.Messages;
18 /// <summary>
19 /// Facilitates customization and creation and an authentication request
20 /// that a Relying Party is preparing to send.
21 /// </summary>
22 internal class AuthenticationRequest : IAuthenticationRequest {
23 /// <summary>
24 /// The name of the internal callback parameter to use to store the user-supplied identifier.
25 /// </summary>
26 internal const string UserSuppliedIdentifierParameterName = "dnoi.userSuppliedIdentifier";
28 /// <summary>
29 /// The relying party that created this request object.
30 /// </summary>
31 private readonly OpenIdRelyingParty RelyingParty;
33 /// <summary>
34 /// The endpoint that describes the particular OpenID Identifier and Provider that
35 /// will be used to create the authentication request.
36 /// </summary>
37 private readonly ServiceEndpoint endpoint;
39 /// <summary>
40 /// The protocol version used at the Provider.
41 /// </summary>
42 private readonly Protocol protocol;
44 /// <summary>
45 /// How an association may or should be created or used in the formulation of the
46 /// authentication request.
47 /// </summary>
48 private AssociationPreference associationPreference = AssociationPreference.IfPossible;
50 /// <summary>
51 /// The extensions that have been added to this authentication request.
52 /// </summary>
53 private List<IOpenIdMessageExtension> extensions = new List<IOpenIdMessageExtension>();
55 /// <summary>
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.
58 /// </summary>
59 private Dictionary<string, string> returnToArgs = new Dictionary<string, string>();
61 /// <summary>
62 /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class.
63 /// </summary>
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;
77 this.Realm = realm;
78 this.ReturnToUrl = returnToUrl;
80 this.Mode = AuthenticationRequestMode.Setup;
83 #region IAuthenticationRequest Members
85 /// <summary>
86 /// Gets or sets the mode the Provider should use during authentication.
87 /// </summary>
88 /// <value></value>
89 public AuthenticationRequestMode Mode { get; set; }
91 /// <summary>
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.
94 /// </summary>
95 /// <value></value>
96 public UserAgentResponse RedirectingResponse {
97 get { return this.RelyingParty.Channel.PrepareResponse(this.CreateRequestMessage()); }
100 /// <summary>
101 /// Gets the URL that the user agent will return to after authentication
102 /// completes or fails at the Provider.
103 /// </summary>
104 /// <value></value>
105 public Uri ReturnToUrl { get; private set; }
107 /// <summary>
108 /// Gets the URL that identifies this consumer web application that
109 /// the Provider will display to the end user.
110 /// </summary>
111 public Realm Realm { get; private set; }
113 /// <summary>
114 /// Gets the Claimed Identifier that the User Supplied Identifier
115 /// resolved to. Null if the user provided an OP Identifier
116 /// (directed identity).
117 /// </summary>
118 /// <value></value>
119 /// <remarks>
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.
127 /// </remarks>
128 public Identifier ClaimedIdentifier {
129 get { return this.IsDirectedIdentity ? null : this.endpoint.ClaimedIdentifier; }
132 /// <summary>
133 /// Gets a value indicating whether the authenticating user has chosen to let the Provider
134 /// determine and send the ClaimedIdentifier after authentication.
135 /// </summary>
136 /// <value></value>
137 public bool IsDirectedIdentity {
138 get { return this.endpoint.ClaimedIdentifier == this.endpoint.Protocol.ClaimedIdentifierForOPIdentifier; }
141 /// <summary>
142 /// Gets information about the OpenId Provider, as advertised by the
143 /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
144 /// location.
145 /// </summary>
146 /// <value></value>
147 IProviderEndpoint IAuthenticationRequest.Provider {
148 get { return this.endpoint; }
151 /// <summary>
152 /// Gets the detected version of OpenID implemented by the Provider.
153 /// </summary>
154 /// <value></value>
155 public Version ProviderVersion {
156 get { return this.endpoint.Protocol.Version; }
159 #endregion
161 /// <summary>
162 /// Gets or sets how an association may or should be created or used
163 /// in the formulation of the authentication request.
164 /// </summary>
165 internal AssociationPreference AssociationPreference {
166 get { return this.associationPreference; }
167 set { this.associationPreference = value; }
170 #region IAuthenticationRequest methods
172 /// <summary>
173 /// Makes a dictionary of key/value pairs available when the authentication is completed.
174 /// </summary>
175 /// <param name="arguments">The arguments to add to the request's return_to URI.</param>
176 /// <remarks>
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>
184 /// </remarks>
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);
197 /// <summary>
198 /// Makes a key/value pair available when the authentication is completed.
199 /// </summary>
200 /// <param name="key">The parameter name.</param>
201 /// <param name="value">The value of the argument.</param>
202 /// <remarks>
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>
210 /// </remarks>
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);
219 /// <summary>
220 /// Adds an OpenID extension to the request directed at the OpenID provider.
221 /// </summary>
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);
228 /// <summary>
229 /// Redirects the user agent to the provider for authentication.
230 /// Execution of the current page terminates after this call.
231 /// </summary>
232 /// <remarks>
233 /// This method requires an ASP.NET HttpContext.
234 /// </remarks>
235 public void RedirectToProvider() {
236 this.RedirectingResponse.Send();
239 #endregion
241 /// <summary>
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.
244 /// </summary>
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);
295 /// <summary>
296 /// Performs deferred request generation for the <see cref="Create"/> method.
297 /// </summary>
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>
304 /// <returns>
305 /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
306 /// </returns>
307 /// <remarks>
308 /// All data validation and cleansing steps must have ALREADY taken place
309 /// before calling this method.
310 /// </remarks>
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);
338 continue;
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;
365 /// <summary>
366 /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
367 /// </summary>
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);
384 } else {
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.");
403 } else {
404 Logger.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
405 Logger.Debug(Util.ToStringDeferred(filteredEndpoints, true));
409 return endpointList;
412 /// <summary>
413 /// Creates the authentication request message to send to the Provider,
414 /// based on the properties in this instance.
415 /// </summary>
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);
432 return request;
435 /// <summary>
436 /// Gets the association to use for this authentication request.
437 /// </summary>
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;
449 break;
450 case AssociationPreference.IfAlreadyEstablished:
451 association = this.RelyingParty.AssociationManager.GetExistingAssociation(this.endpoint.ProviderDescription);
452 break;
453 case AssociationPreference.Never:
454 break;
455 default:
456 throw new InternalErrorException();
459 return association;