//-----------------------------------------------------------------------
//
// 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;
}
}
}