using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Web;
namespace DotNetOpenId.RelyingParty {
///
/// Indicates the mode the Provider should use while authenticating the end user.
///
public enum AuthenticationRequestMode {
///
/// The Provider should use whatever credentials are immediately available
/// to determine whether the end user owns the Identifier. If sufficient
/// credentials (i.e. cookies) are not immediately available, the Provider
/// should fail rather than prompt the user.
///
Immediate,
///
/// The Provider should determine whether the end user owns the Identifier,
/// displaying a web page to the user to login etc., if necessary.
///
Setup
}
[DebuggerDisplay("ClaimedIdentifier: {ClaimedIdentifier}, Mode: {Mode}, OpenId: {protocol.Version}")]
class AuthenticationRequest : IAuthenticationRequest {
internal AssociationPreference associationPreference = AssociationPreference.IfPossible;
ServiceEndpoint endpoint;
Protocol protocol { get { return endpoint.Protocol; } }
internal OpenIdRelyingParty RelyingParty;
AuthenticationRequest(ServiceEndpoint endpoint,
Realm realm, Uri returnToUrl, OpenIdRelyingParty relyingParty) {
if (endpoint == null) throw new ArgumentNullException("endpoint");
if (realm == null) throw new ArgumentNullException("realm");
if (returnToUrl == null) throw new ArgumentNullException("returnToUrl");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
this.endpoint = endpoint;
RelyingParty = relyingParty;
Realm = realm;
ReturnToUrl = returnToUrl;
Mode = AuthenticationRequestMode.Setup;
OutgoingExtensions = ExtensionArgumentsManager.CreateOutgoingExtensions(endpoint.Protocol);
ReturnToArgs = new Dictionary();
}
///
/// Performs identifier discovery and creates associations and generates authentication requests
/// on-demand for as long as new ones can be generated based on the results of Identifier discovery.
///
internal static IEnumerable Create(Identifier userSuppliedIdentifier,
OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, bool createNewAssociationsAsNeeded) {
// We have a long data validation and preparation process
if (userSuppliedIdentifier == null) throw new ArgumentNullException("userSuppliedIdentifier");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (realm == null) throw new ArgumentNullException("realm");
userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment();
if (relyingParty.Settings.RequireSsl) {
// Rather than check for successful SSL conversion at this stage,
// We'll wait for secure discovery to fail on the new identifier.
userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier);
}
Logger.InfoFormat("Creating authentication request for user supplied Identifier: {0}",
userSuppliedIdentifier);
Logger.DebugFormat("Realm: {0}", realm);
Logger.DebugFormat("Return To: {0}", returnToUrl);
Logger.DebugFormat("RequireSsl: {0}", userSuppliedIdentifier.IsDiscoverySecureEndToEnd);
if (Logger.IsWarnEnabled && returnToUrl.Query != null) {
NameValueCollection returnToArgs = HttpUtility.ParseQueryString(returnToUrl.Query);
foreach (string key in returnToArgs) {
if (OpenIdRelyingParty.ShouldParameterBeStrippedFromReturnToUrl(key)) {
Logger.WarnFormat("OpenId argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key);
break;
}
}
}
// 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.
if (!realm.Contains(returnToUrl))
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.ReturnToNotUnderRealm, returnToUrl, realm));
// Perform discovery right now (not deferred).
var serviceEndpoints = userSuppliedIdentifier.Discover();
// Call another method that defers request generation.
return CreateInternal(userSuppliedIdentifier, relyingParty, realm, returnToUrl, serviceEndpoints, createNewAssociationsAsNeeded);
}
///
/// Performs request generation for the method.
/// All data validation and cleansing steps must have ALREADY taken place.
///
private static IEnumerable CreateInternal(Identifier userSuppliedIdentifier,
OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl,
IEnumerable serviceEndpoints, bool createNewAssociationsAsNeeded) {
Logger.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.InfoFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
Logger.DebugFormat("Realm: {0}", realm);
Logger.DebugFormat("Return To: {0}", returnToUrl);
// The strategy here is to prefer endpoints with whom we can create associations.
Association association = null;
if (relyingParty.Store != null) {
// 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 = getAssociation(relyingParty, endpoint, createNewAssociationsAsNeeded);
if (association == null && createNewAssociationsAsNeeded) {
Logger.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.
if (failedAssociationEndpoints.Count > 0) {
Logger.WarnFormat("Now generating requests for Provider endpoints that failed initial association attempts.");
foreach (var endpoint in failedAssociationEndpoints) {
Logger.WarnFormat("Creating authentication request for user supplied Identifier: {0}", userSuppliedIdentifier);
Logger.DebugFormat("Realm: {0}", realm);
Logger.DebugFormat("Return To: {0}", returnToUrl);
// 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;
}
}
}
internal static AuthenticationRequest CreateSingle(Identifier userSuppliedIdentifier,
OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl) {
// Just return the first generated request.
var requests = Create(userSuppliedIdentifier, relyingParty, realm, returnToUrl, true).GetEnumerator();
if (requests.MoveNext()) {
return requests.Current;
} else {
throw new OpenIdException(Strings.OpenIdEndpointNotFound);
}
}
///
/// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
///
private static List filterAndSortEndpoints(IEnumerable endpoints,
OpenIdRelyingParty relyingParty) {
if (endpoints == null) throw new ArgumentNullException("endpoints");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
// Construct the endpoints filters based on criteria given by the host web site.
EndpointSelector versionFilter = ep => ((ServiceEndpoint)ep).Protocol.Version >= Protocol.Lookup(relyingParty.Settings.MinimumRequiredOpenIdVersion).Version;
EndpointSelector hostingSiteFilter = relyingParty.EndpointFilter ?? (ep => true);
bool anyFilteredOut = false;
var filteredEndpoints = new List();
foreach (ServiceEndpoint endpoint in endpoints) {
if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) {
filteredEndpoints.Add(endpoint);
} else {
anyFilteredOut = true;
}
}
// Sort endpoints so that the first one in the list is the most preferred one.
filteredEndpoints.Sort(relyingParty.EndpointOrder);
List endpointList = new List(filteredEndpoints.Count);
foreach (ServiceEndpoint endpoint in filteredEndpoints) {
endpointList.Add(endpoint);
}
if (anyFilteredOut) {
Logger.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count);
}
if (Logger.IsDebugEnabled) {
if (Util.AreSequencesEquivalent(endpoints, endpointList)) {
Logger.Debug("Filtering and sorting of endpoints did not affect the list.");
} else {
Logger.Debug("After filtering and sorting service endpoints, this is the new prioritized list:");
Logger.Debug(Util.ToString(filteredEndpoints, true));
}
}
return endpointList;
}
static Association getAssociation(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, bool createNewAssociationIfNeeded) {
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (provider == null) throw new ArgumentNullException("provider");
// If the RP has no application store for associations, there's no point in creating one.
if (relyingParty.Store == null) {
return null;
}
// TODO: we need a way to lookup an association that fulfills a given set of security
// requirements. We may have a SHA-1 association and a SHA-256 association that need
// to be called for specifically. (a bizzare scenario, admittedly, making this low priority).
Association assoc = relyingParty.Store.GetAssociation(provider.ProviderEndpoint);
// If the returned association does not fulfill security requirements, ignore it.
if (assoc != null && !relyingParty.Settings.IsAssociationInPermittedRange(provider.Protocol, assoc.GetAssociationType(provider.Protocol))) {
assoc = null;
}
if ((assoc == null || !assoc.HasUsefulLifeRemaining) && createNewAssociationIfNeeded) {
var req = AssociateRequest.Create(relyingParty, provider);
if (req == null) {
// this can happen if security requirements and protocol conflict
// to where there are no association types to choose from.
return null;
}
if (req.Response != null) {
// try again if we failed the first time and have a worthy second-try.
if (req.Response.Association == null && req.Response.SecondAttempt != null) {
Logger.Warn("Initial association attempt failed, but will retry with Provider-suggested parameters.");
req = req.Response.SecondAttempt;
}
assoc = req.Response.Association;
// Confirm that the association matches the type we requested (section 8.2.1)
// if this is a 2.0 OP (1.x OPs had freedom to differ from the requested type).
if (assoc != null && provider.Protocol.Version.Major >= 2) {
if (!string.Equals(
req.Args[provider.Protocol.openid.assoc_type],
Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.assoc_type),
StringComparison.Ordinal) ||
!string.Equals(
req.Args[provider.Protocol.openid.session_type],
Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.session_type),
StringComparison.Ordinal)) {
Logger.ErrorFormat("Provider responded with contradicting association parameters. Requested [{0}, {1}] but got [{2}, {3}] back.",
req.Args[provider.Protocol.openid.assoc_type],
req.Args[provider.Protocol.openid.session_type],
Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.assoc_type),
Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.session_type));
assoc = null;
}
}
if (assoc != null) {
Logger.InfoFormat("Association with {0} established.", provider.ProviderEndpoint);
relyingParty.Store.StoreAssociation(provider.ProviderEndpoint, assoc);
} else {
Logger.ErrorFormat("Association attempt with {0} provider failed.", provider.ProviderEndpoint);
}
}
}
return assoc;
}
///
/// Extension arguments to pass to the Provider.
///
protected ExtensionArgumentsManager OutgoingExtensions { get; private set; }
///
/// 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.
///
protected IDictionary ReturnToArgs { get; private set; }
public AuthenticationRequestMode Mode { get; set; }
public Realm Realm { get; private set; }
public Uri ReturnToUrl { get; private set; }
public Identifier ClaimedIdentifier {
get { return IsDirectedIdentity ? null : endpoint.ClaimedIdentifier; }
}
public bool IsDirectedIdentity {
get { return endpoint.ClaimedIdentifier == endpoint.Protocol.ClaimedIdentifierForOPIdentifier; }
}
///
/// The detected version of OpenID implemented by the Provider.
///
public Version ProviderVersion { get { return protocol.Version; } }
///
/// Gets information about the OpenId Provider, as advertised by the
/// OpenId discovery documents found at the
/// location.
///
IProviderEndpoint IAuthenticationRequest.Provider { get { return endpoint; } }
///
/// Gets the response to send to the user agent to begin the
/// OpenID authentication process.
///
public IResponse RedirectingResponse {
get {
UriBuilder returnToBuilder = new UriBuilder(ReturnToUrl);
UriUtil.AppendAndReplaceQueryArgs(returnToBuilder, this.ReturnToArgs);
string token = new Token(endpoint).Serialize(this.RelyingParty.Store);
if (token != null) {
UriUtil.AppendQueryArgs(returnToBuilder, new Dictionary {
{ DotNetOpenId.RelyingParty.Token.TokenKey, token },
});
}
var qsArgs = new Dictionary();
qsArgs.Add(protocol.openid.mode, (Mode == AuthenticationRequestMode.Immediate) ?
protocol.Args.Mode.checkid_immediate : protocol.Args.Mode.checkid_setup);
qsArgs.Add(protocol.openid.identity, endpoint.ProviderLocalIdentifier);
if (endpoint.Protocol.QueryDeclaredNamespaceVersion != null)
qsArgs.Add(protocol.openid.ns, endpoint.Protocol.QueryDeclaredNamespaceVersion);
if (endpoint.Protocol.Version.Major >= 2) {
qsArgs.Add(protocol.openid.claimed_id, endpoint.ClaimedIdentifier);
}
qsArgs.Add(protocol.openid.Realm, Realm);
qsArgs.Add(protocol.openid.return_to, returnToBuilder.Uri.AbsoluteUri);
Association association = null;
if (associationPreference != AssociationPreference.Never) {
association = getAssociation(RelyingParty, endpoint, associationPreference == AssociationPreference.IfPossible);
if (association != null) {
qsArgs.Add(protocol.openid.assoc_handle, association.Handle);
} else {
// Avoid trying to create the association again if the redirecting response
// is generated again.
associationPreference = AssociationPreference.IfAlreadyEstablished;
}
}
// Add on extension arguments
foreach (var pair in OutgoingExtensions.GetArgumentsToSend(true))
qsArgs.Add(pair.Key, pair.Value);
var request = new IndirectMessageRequest(this.endpoint.ProviderEndpoint, qsArgs);
return RelyingParty.Encoder.Encode(request);
}
}
public void AddExtension(DotNetOpenId.Extensions.IExtensionRequest extension) {
if (extension == null) throw new ArgumentNullException("extension");
OutgoingExtensions.AddExtensionArguments(extension.TypeUri, extension.Serialize(this));
}
///
/// Adds given key/value pairs to the query that the provider will use in
/// the request to return to the consumer web site.
///
public void AddCallbackArguments(IDictionary arguments) {
if (arguments == null) throw new ArgumentNullException("arguments");
foreach (var pair in arguments) {
AddCallbackArguments(pair.Key, pair.Value);
}
}
///
/// Adds a given key/value pair to the query that the provider will use in
/// the request to return to the consumer web site.
///
public void AddCallbackArguments(string key, string value) {
if (string.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
if (ReturnToArgs.ContainsKey(key)) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.KeyAlreadyExists, key));
ReturnToArgs.Add(key, value ?? "");
}
///
/// Redirects the user agent to the provider for authentication.
/// Execution of the current page terminates after this call.
///
///
/// This method requires an ASP.NET HttpContext.
///
public void RedirectToProvider() {
if (HttpContext.Current == null || HttpContext.Current.Response == null)
throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
RedirectingResponse.Send();
}
}
}