//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- 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; /// /// Facilitates customization and creation and an authentication request /// that a Relying Party is preparing to send. /// internal class AuthenticationRequest : IAuthenticationRequest { /// /// The name of the internal callback parameter to use to store the user-supplied identifier. /// internal const string UserSuppliedIdentifierParameterName = OpenIdUtilities.CustomParameterPrefix + "userSuppliedIdentifier"; /// /// The relying party that created this request object. /// private readonly OpenIdRelyingParty RelyingParty; /// /// How an association may or should be created or used in the formulation of the /// authentication request. /// private AssociationPreference associationPreference = AssociationPreference.IfPossible; /// /// The extensions that have been added to this authentication request. /// private List extensions = new List(); /// /// 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. /// private Dictionary returnToArgs = new Dictionary(); /// /// A value indicating whether the return_to callback arguments must be signed. /// /// /// 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. /// private bool returnToArgsMustBeSigned; /// /// Initializes a new instance of the class. /// /// The endpoint that describes the OpenID Identifier and Provider that will complete the authentication. /// The realm, or root URL, of the host web site. /// 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 method. /// The relying party that created this instance. private AuthenticationRequest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) { Requires.NotNull(discoveryResult, "discoveryResult"); Requires.NotNull(realm, "realm"); Requires.NotNull(returnToUrl, "returnToUrl"); Requires.NotNull(relyingParty, "relyingParty"); this.DiscoveryResult = discoveryResult; this.RelyingParty = relyingParty; this.Realm = realm; this.ReturnToUrl = returnToUrl; this.Mode = AuthenticationRequestMode.Setup; } #region IAuthenticationRequest Members /// /// Gets or sets the mode the Provider should use during authentication. /// /// public AuthenticationRequestMode Mode { get; set; } /// /// 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. /// /// public OutgoingWebResponse RedirectingResponse { get { foreach (var behavior in this.RelyingParty.Behaviors) { behavior.OnOutgoingAuthenticationRequest(this); } return this.RelyingParty.Channel.PrepareResponse(this.CreateRequestMessage()); } } /// /// Gets the URL that the user agent will return to after authentication /// completes or fails at the Provider. /// /// public Uri ReturnToUrl { get; private set; } /// /// Gets the URL that identifies this consumer web application that /// the Provider will display to the end user. /// public Realm Realm { get; private set; } /// /// Gets the Claimed Identifier that the User Supplied Identifier /// resolved to. Null if the user provided an OP Identifier /// (directed identity). /// /// /// /// 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 /// property rather than testing this /// property for a null value. /// public Identifier ClaimedIdentifier { get { return this.IsDirectedIdentity ? null : this.DiscoveryResult.ClaimedIdentifier; } } /// /// Gets a value indicating whether the authenticating user has chosen to let the Provider /// determine and send the ClaimedIdentifier after authentication. /// public bool IsDirectedIdentity { get { return this.DiscoveryResult.ClaimedIdentifier == this.DiscoveryResult.Protocol.ClaimedIdentifierForOPIdentifier; } } /// /// 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. /// /// /// true if this request is merely a carrier of extensions and is not /// about an OpenID identifier; otherwise, false. /// public bool IsExtensionOnly { get; set; } /// /// Gets information about the OpenId Provider, as advertised by the /// OpenId discovery documents found at the /// location. /// public IProviderEndpoint Provider { get { return this.DiscoveryResult; } } /// /// Gets the discovery result leading to the formulation of this request. /// /// The discovery result. public IdentifierDiscoveryResult DiscoveryResult { get; private set; } #endregion /// /// Gets or sets how an association may or should be created or used /// in the formulation of the authentication request. /// internal AssociationPreference AssociationPreference { get { return this.associationPreference; } set { this.associationPreference = value; } } /// /// Gets the extensions that have been added to the request. /// internal IEnumerable AppliedExtensions { get { return this.extensions; } } /// /// Gets the list of extensions for this request. /// internal IList Extensions { get { return this.extensions; } } #region IAuthenticationRequest methods /// /// Makes a dictionary of key/value pairs available when the authentication is completed. /// /// The arguments to add to the request's return_to URI. /// /// Note that these values are NOT protected against eavesdropping in transit. No /// privacy-sensitive data should be stored using this method. /// The values stored here can be retrieved using /// , which will only return the value /// if it hasn't been tampered with in transit. /// 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. /// public void AddCallbackArguments(IDictionary 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); } } /// /// Makes a key/value pair available when the authentication is completed. /// /// The parameter name. /// The value of the argument. /// /// Note that these values are NOT protected against eavesdropping in transit. No /// privacy-sensitive data should be stored using this method. /// The value stored here can be retrieved using /// , which will only return the value /// if it hasn't been tampered with in transit. /// 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. /// 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); } /// /// Makes a key/value pair available when the authentication is completed. /// /// The parameter name. /// The value of the argument. Must not be null. /// /// Note that these values are NOT protected against tampering in transit. No /// security-sensitive data should be stored using this method. /// The value stored here can be retrieved using /// . /// 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. /// 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; } /// /// 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. /// /// The parameter name. /// The value of the argument. Must not be null. /// /// Note that these values are NOT protected against eavesdropping or tampering in transit. No /// security-sensitive data should be stored using this method. /// The value stored here can be retrieved using /// . /// 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. /// public void SetUntrustedCallbackArgument(string key, string value) { this.returnToArgs[key] = value; } /// /// Adds an OpenID extension to the request directed at the OpenID provider. /// /// The initialized extension to add to the request. public void AddExtension(IOpenIdMessageExtension extension) { this.extensions.Add(extension); } /// /// Redirects the user agent to the provider for authentication. /// /// /// This method requires an ASP.NET HttpContext. /// public void RedirectToProvider() { this.RedirectingResponse.Send(); } #endregion /// /// 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. /// /// The user supplied identifier. /// The relying party. /// The realm. /// The return_to base URL. /// if set to true, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created. /// /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. /// Never null, but may be empty. /// internal static IEnumerable Create(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) { Requires.NotNull(userSuppliedIdentifier, "userSuppliedIdentifier"); Requires.NotNull(relyingParty, "relyingParty"); Requires.NotNull(realm, "realm"); Contract.Ensures(Contract.Result>() != 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 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(); } // Filter disallowed endpoints. serviceEndpoints = relyingParty.SecuritySettings.FilterEndpoints(serviceEndpoints); // Call another method that defers request generation. return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded); } /// /// Creates an instance of FOR TESTING PURPOSES ONLY. /// /// The discovery result. /// The realm. /// The return to. /// The relying party. /// The instantiated . internal static AuthenticationRequest CreateForTest(IdentifierDiscoveryResult discoveryResult, Realm realm, Uri returnTo, OpenIdRelyingParty rp) { return new AuthenticationRequest(discoveryResult, realm, returnTo, rp); } /// /// Creates the request message to send to the Provider, /// based on the properties in this instance. /// /// The message to send to the Provider. internal SignedResponseRequest CreateRequestMessageTestHook() { return this.CreateRequestMessage(); } /// /// Performs deferred request generation for the method. /// /// The user supplied identifier. /// The relying party. /// The realm. /// The return_to base URL. /// The discovered service endpoints on the Claimed Identifier. /// if set to true, associations that do not exist between this Relying Party and the asserting Providers are created before the authentication request is created. /// /// A sequence of authentication requests, any of which constitutes a valid identity assertion on the Claimed Identifier. /// Never null, but may be empty. /// /// /// All data validation and cleansing steps must have ALREADY taken place /// before calling this method. /// private static IEnumerable CreateInternal(Identifier userSuppliedIdentifier, OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IEnumerable 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>() != 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 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(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; } } } } /// /// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier. /// /// The endpoints. /// The relying party. /// A filtered and sorted list of endpoints; may be empty if the input was empty or the filter removed all endpoints. private static List FilterAndSortEndpoints(IEnumerable endpoints, OpenIdRelyingParty relyingParty) { Requires.NotNull(endpoints, "endpoints"); Requires.NotNull(relyingParty, "relyingParty"); bool anyFilteredOut = false; var filteredEndpoints = new List(); 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(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; } /// /// Creates the request message to send to the Provider, /// based on the properties in this instance. /// /// The message to send to the Provider. 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; } /// /// Gets the association to use for this authentication request. /// /// The association to use; null to use 'dumb mode'. 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; } } }