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
.Messages
;
18 /// Facilitates customization and creation and an authentication request
19 /// that a Relying Party is preparing to send.
21 internal class AuthenticationRequest
: IAuthenticationRequest
{
23 /// The name of the internal callback parameter to use to store the user-supplied identifier.
25 internal const string UserSuppliedIdentifierParameterName
= "dnoi.userSuppliedIdentifier";
28 /// The relying party that created this request object.
30 private readonly OpenIdRelyingParty RelyingParty
;
33 /// The endpoint that describes the particular OpenID Identifier and Provider that
34 /// will be used to create the authentication request.
36 private readonly ServiceEndpoint endpoint
;
39 /// The protocol version used at the Provider.
41 private readonly Protocol protocol
;
44 /// How an association may or should be created or used in the formulation of the
45 /// authentication request.
47 private AssociationPreference associationPreference
= AssociationPreference
.IfPossible
;
50 /// The extensions that have been added to this authentication request.
52 private List
<IOpenIdMessageExtension
> extensions
= new List
<IOpenIdMessageExtension
>();
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.
58 private Dictionary
<string, string> returnToArgs
= new Dictionary
<string, string>();
61 /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class.
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
;
77 this.ReturnToUrl
= returnToUrl
;
79 this.Mode
= AuthenticationRequestMode
.Setup
;
82 #region IAuthenticationRequest Members
85 /// Gets or sets the mode the Provider should use during authentication.
88 public AuthenticationRequestMode Mode { get; set; }
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.
95 public UserAgentResponse RedirectingResponse
{
96 get { return this.RelyingParty.Channel.Send(this.CreateRequestMessage()); }
100 /// Gets the URL that the user agent will return to after authentication
101 /// completes or fails at the Provider.
104 public Uri ReturnToUrl { get; private set; }
107 /// Gets the URL that identifies this consumer web application that
108 /// the Provider will display to the end user.
110 public Realm Realm { get; private set; }
113 /// Gets the Claimed Identifier that the User Supplied Identifier
114 /// resolved to. Null if the user provided an OP Identifier
115 /// (directed identity).
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.
127 public Identifier ClaimedIdentifier
{
128 get { return this.IsDirectedIdentity ? null : this.endpoint.ClaimedIdentifier; }
132 /// Gets a value indicating whether the authenticating user has chosen to let the Provider
133 /// determine and send the ClaimedIdentifier after authentication.
136 public bool IsDirectedIdentity
{
137 get { return this.endpoint.ClaimedIdentifier == this.endpoint.Protocol.ClaimedIdentifierForOPIdentifier; }
141 /// Gets information about the OpenId Provider, as advertised by the
142 /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
146 IProviderEndpoint IAuthenticationRequest
.Provider
{
147 get { return this.endpoint; }
151 /// Gets the detected version of OpenID implemented by the Provider.
154 public Version ProviderVersion
{
155 get { return this.endpoint.Protocol.Version; }
159 /// Makes a dictionary of key/value pairs available when the authentication is completed.
161 /// <param name="arguments">The arguments to add to the request's return_to URI.</param>
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>
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
);
180 /// Makes a key/value pair available when the authentication is completed.
182 /// <param name="key">The parameter name.</param>
183 /// <param name="value">The value of the argument.</param>
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>
193 public void AddCallbackArguments(string key
, string value) {
194 this.returnToArgs
.Add(key
, value);
198 /// Adds an OpenID extension to the request directed at the OpenID provider.
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
);
207 /// Redirects the user agent to the provider for authentication.
208 /// Execution of the current page terminates after this call.
211 /// This method requires an ASP.NET HttpContext.
213 public void RedirectToProvider() {
214 this.RedirectingResponse
.Send();
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.
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
);
274 /// Performs deferred request generation for the <see cref="Create"/> method.
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>
283 /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier.
286 /// All data validation and cleansing steps must have ALREADY taken place
287 /// before calling this method.
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
);
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
;
344 /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
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
);
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.");
382 Logger
.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
383 Logger
.Debug(Util
.ToStringDeferred(filteredEndpoints
, true));
391 /// Creates the authentication request message to send to the Provider,
392 /// based on the properties in this instance.
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
);
414 /// Gets the association to use for this authentication request.
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
;
428 case AssociationPreference
.IfAlreadyEstablished
:
429 association
= this.RelyingParty
.GetExistingAssociation(this.endpoint
.ProviderDescription
);
431 case AssociationPreference
.Never
:
434 throw new InternalErrorException();