diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2011-07-20 07:01:58 -0600 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2011-07-20 07:01:58 -0600 |
commit | 1328f88a36187d8aa5890a46e35af59c4df04d3f (patch) | |
tree | c42a3aad4aa21d39b91dcc87a912f8cb96c22c11 /src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs | |
parent | d15895e626b73b6f96f561786b4b5c941c0a4bb1 (diff) | |
download | DotNetOpenAuth-1328f88a36187d8aa5890a46e35af59c4df04d3f.zip DotNetOpenAuth-1328f88a36187d8aa5890a46e35af59c4df04d3f.tar.gz DotNetOpenAuth-1328f88a36187d8aa5890a46e35af59c4df04d3f.tar.bz2 |
Splitting up the OpenID profile into OpenID RP and OP. The core OpenID DLL compiles, but the RP and OP ones do not.
Diffstat (limited to 'src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs new file mode 100644 index 0000000..d79038c --- /dev/null +++ b/src/DotNetOpenAuth.OpenId.RelyingParty/OpenId/RelyingParty/AuthenticationRequest.cs @@ -0,0 +1,594 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.RelyingParty { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using System.Threading; + using System.Web; + + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.ChannelElements; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// Facilitates customization and creation and an authentication request + /// that a Relying Party is preparing to send. + /// </summary> + internal class AuthenticationRequest : IAuthenticationRequest { + /// <summary> + /// The name of the internal callback parameter to use to store the user-supplied identifier. + /// </summary> + internal const string UserSuppliedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "userSuppliedIdentifier"; + + /// <summary> + /// The relying party that created this request object. + /// </summary> + private readonly OpenIdRelyingParty RelyingParty; + + /// <summary> + /// How an association may or should be created or used in the formulation of the + /// authentication request. + /// </summary> + private AssociationPreference associationPreference = AssociationPreference.IfPossible; + + /// <summary> + /// The extensions that have been added to this authentication request. + /// </summary> + private List<IOpenIdMessageExtension> extensions = new List<IOpenIdMessageExtension>(); + + /// <summary> + /// Arguments to add to the return_to part of the query string, so that + /// these values come back to the consumer when the user agent returns. + /// </summary> + private Dictionary<string, string> returnToArgs = new Dictionary<string, string>(); + + /// <summary> + /// A value indicating whether the return_to callback arguments must be signed. + /// </summary> + /// <remarks> + /// This field defaults to false, but is set to true as soon as the first callback argument + /// is added that indicates it must be signed. At which point, all arguments are signed + /// even if individual ones did not need to be. + /// </remarks> + private bool returnToArgsMustBeSigned; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationRequest"/> class. + /// </summary> + /// <param name="discoveryResult">The endpoint that describes the OpenID Identifier and Provider that will complete the authentication.</param> + /// <param name="realm">The realm, or root URL, of the host web site.</param> + /// <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> + /// <param name="relyingParty">The relying party that created this instance.</param> + private AuthenticationRequest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) { + Contract.Requires<ArgumentNullException>(discoveryResult != null); + Contract.Requires<ArgumentNullException>(realm != null); + Contract.Requires<ArgumentNullException>(returnToUrl != null); + Contract.Requires<ArgumentNullException>(relyingParty != null); + + this.DiscoveryResult = discoveryResult; + this.RelyingParty = relyingParty; + this.Realm = realm; + this.ReturnToUrl = returnToUrl; + + this.Mode = AuthenticationRequestMode.Setup; + } + + #region IAuthenticationRequest Members + + /// <summary> + /// Gets or sets the mode the Provider should use during authentication. + /// </summary> + /// <value></value> + public AuthenticationRequestMode Mode { get; set; } + + /// <summary> + /// Gets the HTTP response the relying party should send to the user agent + /// to redirect it to the OpenID Provider to start the OpenID authentication process. + /// </summary> + /// <value></value> + public OutgoingWebResponse RedirectingResponse { + get { + foreach (var behavior in this.RelyingParty.Behaviors) { + behavior.OnOutgoingAuthenticationRequest(this); + } + + return this.RelyingParty.Channel.PrepareResponse(this.CreateRequestMessage()); + } + } + + /// <summary> + /// Gets the URL that the user agent will return to after authentication + /// completes or fails at the Provider. + /// </summary> + /// <value></value> + public Uri ReturnToUrl { get; private set; } + + /// <summary> + /// Gets the URL that identifies this consumer web application that + /// the Provider will display to the end user. + /// </summary> + public Realm Realm { get; private set; } + + /// <summary> + /// Gets the Claimed Identifier that the User Supplied Identifier + /// resolved to. Null if the user provided an OP Identifier + /// (directed identity). + /// </summary> + /// <value></value> + /// <remarks> + /// Null is returned if the user is using the directed identity feature + /// of OpenID 2.0 to make it nearly impossible for a relying party site + /// to improperly store the reserved OpenID URL used for directed identity + /// as a user's own Identifier. + /// However, to test for the Directed Identity feature, please test the + /// <see cref="IsDirectedIdentity"/> property rather than testing this + /// property for a null value. + /// </remarks> + public Identifier ClaimedIdentifier { + get { return this.IsDirectedIdentity ? null : this.DiscoveryResult.ClaimedIdentifier; } + } + + /// <summary> + /// Gets a value indicating whether the authenticating user has chosen to let the Provider + /// determine and send the ClaimedIdentifier after authentication. + /// </summary> + public bool IsDirectedIdentity { + get { return this.DiscoveryResult.ClaimedIdentifier == this.DiscoveryResult.Protocol.ClaimedIdentifierForOPIdentifier; } + } + + /// <summary> + /// Gets or sets a value indicating whether this request only carries extensions + /// and is not a request to verify that the user controls some identifier. + /// </summary> + /// <value> + /// <c>true</c> if this request is merely a carrier of extensions and is not + /// about an OpenID identifier; otherwise, <c>false</c>. + /// </value> + public bool IsExtensionOnly { get; set; } + + /// <summary> + /// Gets information about the OpenId Provider, as advertised by the + /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/> + /// location. + /// </summary> + public IProviderEndpoint Provider { + get { return this.DiscoveryResult; } + } + + /// <summary> + /// Gets the discovery result leading to the formulation of this request. + /// </summary> + /// <value>The discovery result.</value> + public IdentifierDiscoveryResult DiscoveryResult { get; private set; } + + #endregion + + /// <summary> + /// Gets or sets how an association may or should be created or used + /// in the formulation of the authentication request. + /// </summary> + internal AssociationPreference AssociationPreference { + get { return this.associationPreference; } + set { this.associationPreference = value; } + } + + /// <summary> + /// Gets the extensions that have been added to the request. + /// </summary> + internal IEnumerable<IOpenIdMessageExtension> AppliedExtensions { + get { return this.extensions; } + } + + /// <summary> + /// Gets the list of extensions for this request. + /// </summary> + internal IList<IOpenIdMessageExtension> Extensions { + get { return this.extensions; } + } + + #region IAuthenticationRequest methods + + /// <summary> + /// Makes a dictionary of key/value pairs available when the authentication is completed. + /// </summary> + /// <param name="arguments">The arguments to add to the request's return_to URI.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against eavesdropping in transit. No + /// privacy-sensitive data should be stored using this method.</para> + /// <para>The values stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArguments"/>, which will only return the value + /// if it hasn't been tampered with in transit.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void AddCallbackArguments(IDictionary<string, string> arguments) { + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgsMustBeSigned = true; + foreach (var pair in arguments) { + ErrorUtilities.VerifyArgument(!string.IsNullOrEmpty(pair.Key), MessagingStrings.UnexpectedNullOrEmptyKey); + ErrorUtilities.VerifyArgument(pair.Value != null, MessagingStrings.UnexpectedNullValue, pair.Key); + + this.returnToArgs.Add(pair.Key, pair.Value); + } + } + + /// <summary> + /// Makes a key/value pair available when the authentication is completed. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against eavesdropping in transit. No + /// privacy-sensitive data should be stored using this method.</para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>, which will only return the value + /// if it hasn't been tampered with in transit.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void AddCallbackArguments(string key, string value) { + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgsMustBeSigned = true; + this.returnToArgs.Add(key, value); + } + + /// <summary> + /// Makes a key/value pair available when the authentication is completed. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument. Must not be null.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against tampering in transit. No + /// security-sensitive data should be stored using this method.</para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void SetCallbackArgument(string key, string value) { + ErrorUtilities.VerifyOperation(this.RelyingParty.CanSignCallbackArguments, OpenIdStrings.CallbackArgumentsRequireSecretStore, typeof(IRelyingPartyAssociationStore).Name, typeof(OpenIdRelyingParty).Name); + + this.returnToArgsMustBeSigned = true; + this.returnToArgs[key] = value; + } + + /// <summary> + /// Makes a key/value pair available when the authentication is completed without + /// requiring a return_to signature to protect against tampering of the callback argument. + /// </summary> + /// <param name="key">The parameter name.</param> + /// <param name="value">The value of the argument. Must not be null.</param> + /// <remarks> + /// <para>Note that these values are NOT protected against eavesdropping or tampering in transit. No + /// security-sensitive data should be stored using this method. </para> + /// <para>The value stored here can be retrieved using + /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para> + /// <para>Since the data set here is sent in the querystring of the request and some + /// servers place limits on the size of a request URL, this data should be kept relatively + /// small to ensure successful authentication. About 1.5KB is about all that should be stored.</para> + /// </remarks> + public void SetUntrustedCallbackArgument(string key, string value) { + this.returnToArgs[key] = value; + } + + /// <summary> + /// Adds an OpenID extension to the request directed at the OpenID provider. + /// </summary> + /// <param name="extension">The initialized extension to add to the request.</param> + public void AddExtension(IOpenIdMessageExtension extension) { + this.extensions.Add(extension); + } + + /// <summary> + /// Redirects the user agent to the provider for authentication. + /// </summary> + /// <remarks> + /// This method requires an ASP.NET HttpContext. + /// </remarks> + public void RedirectToProvider() { + this.RedirectingResponse.Respond(); + } + + #endregion + + /// <summary> + /// Performs identifier discovery, creates associations and generates authentication requests + /// on-demand for as long as new ones can be generated based on the results of Identifier discovery. + /// </summary> + /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> + /// <param name="relyingParty">The relying party.</param> + /// <param name="realm">The realm.</param> + /// <param name="returnToUrl">The return_to base URL.</param> + /// <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> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + internal static IEnumerable<AuthenticationRequest> Create(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) { + Contract.Requires<ArgumentNullException>(userSuppliedIdentifier != null); + Contract.Requires<ArgumentNullException>(relyingParty != null); + Contract.Requires<ArgumentNullException>(realm != null); + Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null); + + // Normalize the portion of the return_to path that correlates to the realm for capitalization. + // (so that if a web app base path is /MyApp/, but the URL of this request happens to be + // /myapp/login.aspx, we bump up the return_to Url to use /MyApp/ so it matches the realm. + UriBuilder returnTo = new UriBuilder(returnToUrl); + if (returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.OrdinalIgnoreCase) && + !returnTo.Path.StartsWith(realm.AbsolutePath, StringComparison.Ordinal)) { + returnTo.Path = realm.AbsolutePath + returnTo.Path.Substring(realm.AbsolutePath.Length); + returnToUrl = returnTo.Uri; + } + + userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment(); + if (relyingParty.SecuritySettings.RequireSsl) { + // Rather than check for successful SSL conversion at this stage, + // We'll wait for secure discovery to fail on the new identifier. + if (!userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier)) { + // But at least log the failure. + Logger.OpenId.WarnFormat("RequireSsl mode is on, so discovery on insecure identifier {0} will yield no results.", userSuppliedIdentifier); + } + } + + if (Logger.OpenId.IsWarnEnabled && returnToUrl.Query != null) { + NameValueCollection returnToArgs = HttpUtility.ParseQueryString(returnToUrl.Query); + foreach (string key in returnToArgs) { + if (OpenIdRelyingParty.IsOpenIdSupportingParameter(key)) { + Logger.OpenId.WarnFormat("OpenID argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key); + } + } + } + + // Throw an exception now if the realm and the return_to URLs don't match + // as required by the provider. We could wait for the provider to test this and + // fail, but this will be faster and give us a better error message. + ErrorUtilities.VerifyProtocol(realm.Contains(returnToUrl), OpenIdStrings.ReturnToNotUnderRealm, returnToUrl, realm); + + // Perform discovery right now (not deferred). + IEnumerable<IdentifierDiscoveryResult> serviceEndpoints; + try { + var results = relyingParty.Discover(userSuppliedIdentifier).CacheGeneratedResults(); + + // If any OP Identifier service elements were found, we must not proceed + // to use any Claimed Identifier services, per OpenID 2.0 sections 7.3.2.2 and 11.2. + // For a discussion on this topic, see + // http://groups.google.com/group/dotnetopenid/browse_thread/thread/4b5a8c6b2210f387/5e25910e4d2252c8 + // Usually the Discover method we called will automatically filter this for us, but + // just to be sure, we'll do it here as well since the RP may be configured to allow + // these dual identifiers for assertion verification purposes. + var opIdentifiers = results.Where(result => result.ClaimedIdentifier == result.Protocol.ClaimedIdentifierForOPIdentifier).CacheGeneratedResults(); + var claimedIdentifiers = results.Where(result => result.ClaimedIdentifier != result.Protocol.ClaimedIdentifierForOPIdentifier); + serviceEndpoints = opIdentifiers.Any() ? opIdentifiers : claimedIdentifiers; + } catch (ProtocolException ex) { + Logger.Yadis.ErrorFormat("Error while performing discovery on: \"{0}\": {1}", userSuppliedIdentifier, ex); + serviceEndpoints = Enumerable.Empty<IdentifierDiscoveryResult>(); + } + + // Filter disallowed endpoints. + serviceEndpoints = relyingParty.SecuritySettings.FilterEndpoints(serviceEndpoints); + + // Call another method that defers request generation. + return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded); + } + + /// <summary> + /// Creates an instance of <see cref="AuthenticationRequest"/> FOR TESTING PURPOSES ONLY. + /// </summary> + /// <param name="discoveryResult">The discovery result.</param> + /// <param name="realm">The realm.</param> + /// <param name="returnTo">The return to.</param> + /// <param name="rp">The relying party.</param> + /// <returns>The instantiated <see cref="AuthenticationRequest"/>.</returns> + internal static AuthenticationRequest CreateForTest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnTo, OpenIdRelyingParty rp) { + return new AuthenticationRequest(discoveryResult, realm, returnTo, rp); + } + + /// <summary> + /// Creates the request message to send to the Provider, + /// based on the properties in this instance. + /// </summary> + /// <returns>The message to send to the Provider.</returns> + internal SignedResponseRequest CreateRequestMessageTestHook() + { + return this.CreateRequestMessage(); + } + + /// <summary> + /// Performs deferred request generation for the <see cref="Create"/> method. + /// </summary> + /// <param name="userSuppliedIdentifier">The user supplied identifier.</param> + /// <param name="relyingParty">The relying party.</param> + /// <param name="realm">The realm.</param> + /// <param name="returnToUrl">The return_to base URL.</param> + /// <param name="serviceEndpoints">The discovered service endpoints on the Claimed Identifier.</param> + /// <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> + /// <returns> + /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. + /// Never null, but may be empty. + /// </returns> + /// <remarks> + /// All data validation and cleansing steps must have ALREADY taken place + /// before calling this method. + /// </remarks> + private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IEnumerable<IdentifierDiscoveryResult> serviceEndpoints, bool createNewAssociationsAsNeeded) { + // DO NOT USE CODE CONTRACTS IN THIS METHOD, since it uses yield return + ErrorUtilities.VerifyArgumentNotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); + ErrorUtilities.VerifyArgumentNotNull(relyingParty, "relyingParty"); + ErrorUtilities.VerifyArgumentNotNull(realm, "realm"); + ErrorUtilities.VerifyArgumentNotNull(serviceEndpoints, "serviceEndpoints"); + ////Contract.Ensures(Contract.Result<IEnumerable<AuthenticationRequest>>() != null); + + // If shared associations are required, then we had better have an association store. + ErrorUtilities.VerifyOperation(!relyingParty.SecuritySettings.RequireAssociation || relyingParty.AssociationManager.HasAssociationStore, OpenIdStrings.AssociationStoreRequired); + + Logger.Yadis.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier); + IEnumerable<IdentifierDiscoveryResult> endpoints = FilterAndSortEndpoints(serviceEndpoints, relyingParty); + + // Maintain a list of endpoints that we could not form an association with. + // We'll fallback to generating requests to these if the ones we CAN create + // an association with run out. + var failedAssociationEndpoints = new List<IdentifierDiscoveryResult>(0); + + foreach (var endpoint in endpoints) { + Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier); + + // The strategy here is to prefer endpoints with whom we can create associations. + Association association = null; + if (relyingParty.AssociationManager.HasAssociationStore) { + // In some scenarios (like the AJAX control wanting ALL auth requests possible), + // we don't want to create associations with every Provider. But we'll use + // associations where they are already formed from previous authentications. + association = createNewAssociationsAsNeeded ? relyingParty.AssociationManager.GetOrCreateAssociation(endpoint) : relyingParty.AssociationManager.GetExistingAssociation(endpoint); + if (association == null && createNewAssociationsAsNeeded) { + Logger.OpenId.WarnFormat("Failed to create association with {0}. Skipping to next endpoint.", endpoint.ProviderEndpoint); + + // No association could be created. Add it to the list of failed association + // endpoints and skip to the next available endpoint. + failedAssociationEndpoints.Add(endpoint); + continue; + } + } + + yield return new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty); + } + + // Now that we've run out of endpoints that respond to association requests, + // since we apparently are still running, the caller must want another request. + // We'll go ahead and generate the requests to OPs that may be down -- + // unless associations are set as required in our security settings. + if (failedAssociationEndpoints.Count > 0) { + if (relyingParty.SecuritySettings.RequireAssociation) { + Logger.OpenId.Warn("Associations could not be formed with some Providers. Security settings require shared associations for authentication requests so these will be skipped."); + } else { + Logger.OpenId.Debug("Now generating requests for Provider endpoints that failed initial association attempts."); + + foreach (var endpoint in failedAssociationEndpoints) { + Logger.OpenId.DebugFormat("Creating authentication request for user supplied Identifier: {0} at endpoint: {1}", userSuppliedIdentifier, endpoint.ProviderEndpoint.AbsoluteUri); + + // Create the auth request, but prevent it from attempting to create an association + // because we've already tried. Let's not have it waste time trying again. + var authRequest = new AuthenticationRequest(endpoint, realm, returnToUrl, relyingParty); + authRequest.associationPreference = AssociationPreference.IfAlreadyEstablished; + yield return authRequest; + } + } + } + } + + /// <summary> + /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier. + /// </summary> + /// <param name="endpoints">The endpoints.</param> + /// <param name="relyingParty">The relying party.</param> + /// <returns>A filtered and sorted list of endpoints; may be empty if the input was empty or the filter removed all endpoints.</returns> + private static List<IdentifierDiscoveryResult> FilterAndSortEndpoints(IEnumerable<IdentifierDiscoveryResult> endpoints, OpenIdRelyingParty relyingParty) { + Contract.Requires<ArgumentNullException>(endpoints != null); + Contract.Requires<ArgumentNullException>(relyingParty != null); + + bool anyFilteredOut = false; + var filteredEndpoints = new List<IdentifierDiscoveryResult>(); + foreach (var endpoint in endpoints) { + if (relyingParty.FilterEndpoint(endpoint)) { + filteredEndpoints.Add(endpoint); + } else { + anyFilteredOut = true; + } + } + + // Sort endpoints so that the first one in the list is the most preferred one. + filteredEndpoints.OrderBy(ep => ep, relyingParty.EndpointOrder); + + var endpointList = new List<IdentifierDiscoveryResult>(filteredEndpoints.Count); + foreach (var endpoint in filteredEndpoints) { + endpointList.Add(endpoint); + } + + if (anyFilteredOut) { + Logger.Yadis.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count); + } + if (Logger.Yadis.IsDebugEnabled) { + if (MessagingUtilities.AreEquivalent(endpoints, endpointList)) { + Logger.Yadis.Debug("Filtering and sorting of endpoints did not affect the list."); + } else { + Logger.Yadis.Debug("After filtering and sorting service endpoints, this is the new prioritized list:"); + Logger.Yadis.Debug(Util.ToStringDeferred(filteredEndpoints, true)); + } + } + + return endpointList; + } + + /// <summary> + /// Creates the request message to send to the Provider, + /// based on the properties in this instance. + /// </summary> + /// <returns>The message to send to the Provider.</returns> + private SignedResponseRequest CreateRequestMessage() { + Association association = this.GetAssociation(); + + SignedResponseRequest request; + if (!this.IsExtensionOnly) { + CheckIdRequest authRequest = new CheckIdRequest(this.DiscoveryResult.Version, this.DiscoveryResult.ProviderEndpoint, this.Mode); + authRequest.ClaimedIdentifier = this.DiscoveryResult.ClaimedIdentifier; + authRequest.LocalIdentifier = this.DiscoveryResult.ProviderLocalIdentifier; + request = authRequest; + } else { + request = new SignedResponseRequest(this.DiscoveryResult.Version, this.DiscoveryResult.ProviderEndpoint, this.Mode); + } + request.Realm = this.Realm; + request.ReturnTo = this.ReturnToUrl; + request.AssociationHandle = association != null ? association.Handle : null; + request.SignReturnTo = this.returnToArgsMustBeSigned; + request.AddReturnToArguments(this.returnToArgs); + if (this.DiscoveryResult.UserSuppliedIdentifier != null && OpenIdElement.Configuration.RelyingParty.PreserveUserSuppliedIdentifier) { + request.AddReturnToArguments(UserSuppliedIdentifierParameterName, this.DiscoveryResult.UserSuppliedIdentifier.OriginalString); + } + foreach (IOpenIdMessageExtension extension in this.extensions) { + request.Extensions.Add(extension); + } + + return request; + } + + /// <summary> + /// Gets the association to use for this authentication request. + /// </summary> + /// <returns>The association to use; <c>null</c> to use 'dumb mode'.</returns> + private Association GetAssociation() { + Association association = null; + switch (this.associationPreference) { + case AssociationPreference.IfPossible: + association = this.RelyingParty.AssociationManager.GetOrCreateAssociation(this.DiscoveryResult); + if (association == null) { + // Avoid trying to create the association again if the redirecting response + // is generated again. + this.associationPreference = AssociationPreference.IfAlreadyEstablished; + } + break; + case AssociationPreference.IfAlreadyEstablished: + association = this.RelyingParty.AssociationManager.GetExistingAssociation(this.DiscoveryResult); + break; + case AssociationPreference.Never: + break; + default: + throw new InternalErrorException(); + } + + return association; + } + } +} |