Added some tests for OpenIdRelyingParty.
[dotnetoauth.git] / src / DotNetOpenAuth / OpenId / RelyingParty / AuthenticationRequest.cs
blob20ac2cbca10d68d631733d2c6a99c8eefd5e940c
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.Messages;
17 /// <summary>
18 /// Facilitates customization and creation and an authentication request
19 /// that a Relying Party is preparing to send.
20 /// </summary>
21 internal class AuthenticationRequest : IAuthenticationRequest {
22 /// <summary>
23 /// The name of the internal callback parameter to use to store the user-supplied identifier.
24 /// </summary>
25 internal const string UserSuppliedIdentifierParameterName = "dnoi.userSuppliedIdentifier";
27 /// <summary>
28 /// The relying party that created this request object.
29 /// </summary>
30 private readonly OpenIdRelyingParty RelyingParty;
32 /// <summary>
33 /// The endpoint that describes the particular OpenID Identifier and Provider that
34 /// will be used to create the authentication request.
35 /// </summary>
36 private readonly ServiceEndpoint endpoint;
38 /// <summary>
39 /// The protocol version used at the Provider.
40 /// </summary>
41 private readonly Protocol protocol;
43 /// <summary>
44 /// How an association may or should be created or used in the formulation of the
45 /// authentication request.
46 /// </summary>
47 private AssociationPreference associationPreference = AssociationPreference.IfPossible;
49 /// <summary>
50 /// The extensions that have been added to this authentication request.
51 /// </summary>
52 private List<IOpenIdMessageExtension> extensions = new List<IOpenIdMessageExtension>();
54 /// <summary>
55 /// Arguments to add to the return_to part of the query string, so that
56 /// these values come back to the consumer when the user agent returns.
57 /// </summary>
58 private Dictionary<string, string> returnToArgs = new Dictionary<string, string>();
60 /// <summary>
61 /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class.
62 /// </summary>
63 /// <param name="endpoint">The endpoint that describes the OpenID Identifier and Provider that will complete the authentication.</param>
64 /// <param name="realm">The realm, or root URL, of the host web site.</param>
65 /// <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>
66 /// <param name="relyingParty">The relying party that created this instance.</param>
67 private AuthenticationRequest(ServiceEndpoint endpoint, Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) {
68 ErrorUtilities.VerifyArgumentNotNull(endpoint, "endpoint");
69 ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
70 ErrorUtilities.VerifyArgumentNotNull(returnToUrl, "returnToUrl");
71 ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty");
73 this.endpoint = endpoint;
74 this.protocol = endpoint.Protocol;
75 this.RelyingParty = relyingParty;
76 this.Realm = realm;
77 this.ReturnToUrl = returnToUrl;
79 this.Mode = AuthenticationRequestMode.Setup;
82 #region IAuthenticationRequest Members
84 /// <summary>
85 /// Gets or sets the mode the Provider should use during authentication.
86 /// </summary>
87 /// <value></value>
88 public AuthenticationRequestMode Mode { get; set; }
90 /// <summary>
91 /// Gets the HTTP response the relying party should send to the user agent
92 /// to redirect it to the OpenID Provider to start the OpenID authentication process.
93 /// </summary>
94 /// <value></value>
95 public UserAgentResponse RedirectingResponse {
96 get { return this.RelyingParty.Channel.Send(this.CreateRequestMessage()); }
99 /// <summary>
100 /// Gets the URL that the user agent will return to after authentication
101 /// completes or fails at the Provider.
102 /// </summary>
103 /// <value></value>
104 public Uri ReturnToUrl { get; private set; }
106 /// <summary>
107 /// Gets the URL that identifies this consumer web application that
108 /// the Provider will display to the end user.
109 /// </summary>
110 public Realm Realm { get; private set; }
112 /// <summary>
113 /// Gets the Claimed Identifier that the User Supplied Identifier
114 /// resolved to. Null if the user provided an OP Identifier
115 /// (directed identity).
116 /// </summary>
117 /// <value></value>
118 /// <remarks>
119 /// Null is returned if the user is using the directed identity feature
120 /// of OpenID 2.0 to make it nearly impossible for a relying party site
121 /// to improperly store the reserved OpenID URL used for directed identity
122 /// as a user's own Identifier.
123 /// However, to test for the Directed Identity feature, please test the
124 /// <see cref="IsDirectedIdentity"/> property rather than testing this
125 /// property for a null value.
126 /// </remarks>
127 public Identifier ClaimedIdentifier {
128 get { return this.IsDirectedIdentity ? null : this.endpoint.ClaimedIdentifier; }
131 /// <summary>
132 /// Gets a value indicating whether the authenticating user has chosen to let the Provider
133 /// determine and send the ClaimedIdentifier after authentication.
134 /// </summary>
135 /// <value></value>
136 public bool IsDirectedIdentity {
137 get { return this.endpoint.ClaimedIdentifier == this.endpoint.Protocol.ClaimedIdentifierForOPIdentifier; }
140 /// <summary>
141 /// Gets information about the OpenId Provider, as advertised by the
142 /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
143 /// location.
144 /// </summary>
145 /// <value></value>
146 IProviderEndpoint IAuthenticationRequest.Provider {
147 get { return this.endpoint; }
150 /// <summary>
151 /// Gets the detected version of OpenID implemented by the Provider.
152 /// </summary>
153 /// <value></value>
154 public Version ProviderVersion {
155 get { return this.endpoint.Protocol.Version; }
158 /// <summary>
159 /// Makes a dictionary of key/value pairs available when the authentication is completed.
160 /// </summary>
161 /// <param name="arguments">The arguments to add to the request's return_to URI.</param>
162 /// <remarks>
163 /// <para>Note that these values are NOT protected against tampering in transit. No
164 /// security-sensitive data should be stored using this method.</para>
165 /// <para>The values stored here can be retrieved using
166 /// <see cref="IAuthenticationResponse.GetCallbackArguments"/>.</para>
167 /// <para>Since the data set here is sent in the querystring of the request and some
168 /// servers place limits on the size of a request URL, this data should be kept relatively
169 /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
170 /// </remarks>
171 public void AddCallbackArguments(IDictionary<string, string> arguments) {
172 ErrorUtilities.VerifyArgumentNotNull(arguments, "arguments");
174 foreach (var pair in arguments) {
175 this.returnToArgs.Add(pair.Key, pair.Value);
179 /// <summary>
180 /// Makes a key/value pair available when the authentication is completed.
181 /// </summary>
182 /// <param name="key">The parameter name.</param>
183 /// <param name="value">The value of the argument.</param>
184 /// <remarks>
185 /// <para>Note that these values are NOT protected against tampering in transit. No
186 /// security-sensitive data should be stored using this method.</para>
187 /// <para>The value stored here can be retrieved using
188 /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
189 /// <para>Since the data set here is sent in the querystring of the request and some
190 /// servers place limits on the size of a request URL, this data should be kept relatively
191 /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para>
192 /// </remarks>
193 public void AddCallbackArguments(string key, string value) {
194 this.returnToArgs.Add(key, value);
197 /// <summary>
198 /// Adds an OpenID extension to the request directed at the OpenID provider.
199 /// </summary>
200 /// <param name="extension">The initialized extension to add to the request.</param>
201 public void AddExtension(IOpenIdMessageExtension extension) {
202 ErrorUtilities.VerifyArgumentNotNull(extension, "extension");
203 this.extensions.Add(extension);
206 /// <summary>
207 /// Redirects the user agent to the provider for authentication.
208 /// Execution of the current page terminates after this call.
209 /// </summary>
210 /// <remarks>
211 /// This method requires an ASP.NET HttpContext.
212 /// </remarks>
213 public void RedirectToProvider() {
214 this.RedirectingResponse.Send();
217 #endregion
219 /// <summary>
220 /// Performs identifier discovery, creates associations and generates authentication requests
221 /// on-demand for as long as new ones can be generated based on the results of Identifier discovery.
222 /// </summary>
223 /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
224 /// <param name="relyingParty">The relying party.</param>
225 /// <param name="realm">The realm.</param>
226 /// <param name="returnToUrl">The return_to base URL.</param>
227 /// <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>
228 /// <returns>A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.</returns>
229 internal static IEnumerable<AuthenticationRequest> Create(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) {
230 // We have a long data validation and preparation process
231 ErrorUtilities.VerifyArgumentNotNull(userSuppliedIdentifier, "userSuppliedIdentifier");
232 ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty");
233 ErrorUtilities.VerifyArgumentNotNull(realm, "realm");
235 // Normalize the portion of the return_to path that correlates to the realm for capitalization.
236 // (so that if a web app base path is /MyApp/, but the URL of this request happens to be
237 // /myapp/login.aspx, we bump up the return_to Url to use /MyApp/ so it matches the realm.
238 UriBuilder returnTo = new UriBuilder(returnToUrl);
239 if (returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.OrdinalIgnoreCase) &&
240 !returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.Ordinal)) {
241 returnTo.Path = realm.AbsolutePath + returnTo.Path.Substring(realm.AbsolutePath.Length);
242 returnToUrl = returnTo.Uri;
245 userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment();
246 if (relyingParty.SecuritySettings.RequireSsl) {
247 // Rather than check for successful SSL conversion at this stage,
248 // We'll wait for secure discovery to fail on the new identifier.
249 userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier);
252 if (Logger.IsWarnEnabled && returnToUrl.Query != null) {
253 NameValueCollection returnToArgs = HttpUtility.ParseQueryString(returnToUrl.Query);
254 foreach (string key in returnToArgs) {
255 if (OpenIdRelyingParty.IsOpenIdSupportingParameter(key)) {
256 Logger.WarnFormat("OpenID argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key);
261 // Throw an exception now if the realm and the return_to URLs don't match
262 // as required by the provider. We could wait for the provider to test this and
263 // fail, but this will be faster and give us a better error message.
264 ErrorUtilities.VerifyProtocol(realm.Contains(returnToUrl), OpenIdStrings.ReturnToNotUnderRealm, returnToUrl, realm);
266 // Perform discovery right now (not deferred).
267 var serviceEndpoints = userSuppliedIdentifier.Discover(relyingParty.WebRequestHandler);
269 // Call another method that defers request generation.
270 return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded);
273 /// <summary>
274 /// Performs deferred request generation for the <see cref="Create"/> method.
275 /// </summary>
276 /// <param name="userSuppliedIdentifier">The user supplied identifier.</param>
277 /// <param name="relyingParty">The relying party.</param>
278 /// <param name="realm">The realm.</param>
279 /// <param name="returnToUrl">The return_to base URL.</param>
280 /// <param name="serviceEndpoints">The discovered service endpoints on the Claimed Identifier.</param>
281 /// <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>
282 /// <returns>
283 /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
284 /// </returns>
285 /// <remarks>
286 /// All data validation and cleansing steps must have ALREADY taken place
287 /// before calling this method.
288 /// </remarks>
289 private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IEnumerable<ServiceEndpoint> serviceEndpoints, bool createNewAssociationsAsNeeded) {
290 Logger.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier);
291 IEnumerable<ServiceEndpoint> endpoints = FilterAndSortEndpoints(serviceEndpoints, relyingParty);
293 // Maintain a list of endpoints that we could not form an association with.
294 // We'll fallback to generating requests to these if the ones we CAN create
295 // an association with run out.
296 var failedAssociationEndpoints = new List<ServiceEndpoint>(0);
298 foreach (var endpoint in endpoints) {
299 Logger.InfoFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
300 Logger.DebugFormat("Realm: {0}", realm);
301 Logger.DebugFormat("Return To: {0}", returnToUrl);
303 // The strategy here is to prefer endpoints with whom we can create associations.
304 Association association = null;
305 if (relyingParty.AssociationStore != null) {
306 // In some scenarios (like the AJAX control wanting ALL auth requests possible),
307 // we don't want to create associations with every Provider. But we'll use
308 // associations where they are already formed from previous authentications.
309 association = createNewAssociationsAsNeeded ? relyingParty.GetOrCreateAssociation(endpoint.ProviderDescription) : relyingParty.GetExistingAssociation(endpoint.ProviderDescription);
310 if (association == null && createNewAssociationsAsNeeded) {
311 Logger.WarnFormat("Failed to create association with {0}. Skipping to next endpoint.", endpoint.ProviderEndpoint);
313 // No association could be created. Add it to the list of failed association
314 // endpoints and skip to the next available endpoint.
315 failedAssociationEndpoints.Add(endpoint);
316 continue;
320 yield return new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty);
323 // Now that we've run out of endpoints that respond to association requests,
324 // since we apparently are still running, the caller must want another request.
325 // We'll go ahead and generate the requests to OPs that may be down.
326 if (failedAssociationEndpoints.Count > 0) {
327 Logger.WarnFormat("Now generating requests for Provider endpoints that failed initial association attempts.");
329 foreach (var endpoint in failedAssociationEndpoints) {
330 Logger.WarnFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
331 Logger.DebugFormat("Realm: {0}", realm);
332 Logger.DebugFormat("Return To: {0}", returnToUrl);
334 // Create the auth request, but prevent it from attempting to create an association
335 // because we've already tried. Let's not have it waste time trying again.
336 var authRequest = new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty);
337 authRequest.associationPreference = AssociationPreference.IfAlreadyEstablished;
338 yield return authRequest;
343 /// <summary>
344 /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
345 /// </summary>
346 /// <param name="endpoints">The endpoints.</param>
347 /// <param name="relyingParty">The relying party.</param>
348 /// <returns>A filtered and sorted list of endpoints; may be empty if the input was empty or the filter removed all endpoints.</returns>
349 private static List<ServiceEndpoint> FilterAndSortEndpoints(IEnumerable<ServiceEndpoint> endpoints, OpenIdRelyingParty relyingParty) {
350 ErrorUtilities.VerifyArgumentNotNull(endpoints, "endpoints");
351 ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty");
353 // Construct the endpoints filters based on criteria given by the host web site.
354 EndpointSelector versionFilter = ep => ((ServiceEndpoint)ep).Protocol.Version >= Protocol.Lookup(relyingParty.SecuritySettings.MinimumRequiredOpenIdVersion).Version;
355 EndpointSelector hostingSiteFilter = relyingParty.EndpointFilter ?? (ep => true);
357 bool anyFilteredOut = false;
358 var filteredEndpoints = new List<IXrdsProviderEndpoint>();
359 foreach (ServiceEndpoint endpoint in endpoints) {
360 if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) {
361 filteredEndpoints.Add(endpoint);
362 } else {
363 anyFilteredOut = true;
367 // Sort endpoints so that the first one in the list is the most preferred one.
368 filteredEndpoints.Sort(relyingParty.EndpointOrder);
370 List<ServiceEndpoint> endpointList = new List<ServiceEndpoint>(filteredEndpoints.Count);
371 foreach (ServiceEndpoint endpoint in filteredEndpoints) {
372 endpointList.Add(endpoint);
375 if (anyFilteredOut) {
376 Logger.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count);
378 if (Logger.IsDebugEnabled) {
379 if (MessagingUtilities.AreEquivalent(endpoints, endpointList)) {
380 Logger.Debug("Filtering and sorting of endpoints did not affect the list.");
381 } else {
382 Logger.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
383 Logger.Debug(Util.ToStringDeferred(filteredEndpoints, true));
387 return endpointList;
390 /// <summary>
391 /// Creates the authentication request message to send to the Provider,
392 /// based on the properties in this instance.
393 /// </summary>
394 /// <returns>The message to send to the Provider.</returns>
395 private CheckIdRequest CreateRequestMessage() {
396 Association association = this.GetAssociation();
398 CheckIdRequest request = new CheckIdRequest(this.ProviderVersion, this.endpoint.ProviderEndpoint, this.Mode);
399 request.ClaimedIdentifier = this.endpoint.ClaimedIdentifier;
400 request.LocalIdentifier = this.endpoint.ProviderLocalIdentifier;
401 request.Realm = this.Realm;
402 request.ReturnTo = this.ReturnToUrl;
403 request.AssociationHandle = association != null ? association.Handle : null;
404 request.AddReturnToArguments(this.returnToArgs);
405 request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.endpoint.UserSuppliedIdentifier);
406 foreach (IOpenIdMessageExtension extension in this.extensions) {
407 request.Extensions.Add(extension);
410 return request;
413 /// <summary>
414 /// Gets the association to use for this authentication request.
415 /// </summary>
416 /// <returns>The association to use; <c>null</c> to use 'dumb mode'.</returns>
417 private Association GetAssociation() {
418 Association association = null;
419 switch (this.associationPreference) {
420 case AssociationPreference.IfPossible:
421 association = this.RelyingParty.GetOrCreateAssociation(this.endpoint.ProviderDescription);
422 if (association == null) {
423 // Avoid trying to create the association again if the redirecting response
424 // is generated again.
425 this.associationPreference = AssociationPreference.IfAlreadyEstablished;
427 break;
428 case AssociationPreference.IfAlreadyEstablished:
429 association = this.RelyingParty.GetExistingAssociation(this.endpoint.ProviderDescription);
430 break;
431 case AssociationPreference.Never:
432 break;
433 default:
434 throw new InternalErrorException();
437 return association;