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 {
Association assoc;
ServiceEndpoint endpoint;
Protocol protocol { get { return endpoint.Protocol; } }
internal OpenIdRelyingParty RelyingParty;
AuthenticationRequest(string token, Association assoc, 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.assoc = assoc;
this.endpoint = endpoint;
RelyingParty = relyingParty;
Realm = realm;
ReturnToUrl = returnToUrl;
Mode = AuthenticationRequestMode.Setup;
OutgoingExtensions = ExtensionArgumentsManager.CreateOutgoingExtensions(endpoint.Protocol);
ReturnToArgs = new Dictionary();
if (token != null)
AddCallbackArguments(DotNetOpenId.RelyingParty.Token.TokenKey, token);
}
internal static AuthenticationRequest Create(Identifier userSuppliedIdentifier,
OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl) {
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);
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;
}
}
}
var endpoints = new List(userSuppliedIdentifier.Discover());
ServiceEndpoint endpoint = selectEndpoint(endpoints.AsReadOnly(), relyingParty);
if (endpoint == null)
throw new OpenIdException(Strings.OpenIdEndpointNotFound);
// 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));
string token = new Token(endpoint).Serialize(relyingParty.Store);
// Retrieve the association, but don't create one, as a creation was already
// attempted by the selectEndpoint method.
Association association = relyingParty.Store != null ? getAssociation(relyingParty, endpoint, false) : null;
return new AuthenticationRequest(
token, association, endpoint, realm, returnToUrl, relyingParty);
}
///
/// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
///
private static List filterAndSortEndpoints(ReadOnlyCollection 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);
var filteredEndpoints = new List(endpoints.Count);
foreach (ServiceEndpoint endpoint in endpoints) {
if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) {
filteredEndpoints.Add(endpoint);
}
}
// 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);
}
return endpointList;
}
///
/// Chooses which provider endpoint is the best one to use.
///
/// The best endpoint, or null if no acceptable endpoints were found.
private static ServiceEndpoint selectEndpoint(ReadOnlyCollection endpoints,
OpenIdRelyingParty relyingParty) {
List filteredEndpoints = filterAndSortEndpoints(endpoints, relyingParty);
if (filteredEndpoints.Count != endpoints.Count) {
Logger.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count);
}
if (Logger.IsDebugEnabled) {
if (Util.AreSequencesEquivalent(endpoints, filteredEndpoints)) {
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));
}
}
// If there are no endpoint candidates...
if (filteredEndpoints.Count == 0) {
return null;
}
// If we don't have an application store, we have no place to record an association to
// and therefore can only take our best shot at one of the endpoints.
if (relyingParty.Store == null) {
Logger.Debug("No state store, so the first endpoint available is selected.");
return filteredEndpoints[0];
}
// Go through each endpoint until we find one that we can successfully create
// an association with. This is our only hint about whether an OP is up and running.
// The idea here is that we don't want to redirect the user to a dead OP for authentication.
// If the user has multiple OPs listed in his/her XRDS document, then we'll go down the list
// and try each one until we find one that's good.
int winningEndpointIndex = 0;
foreach (ServiceEndpoint endpointCandidate in filteredEndpoints) {
winningEndpointIndex++;
// One weakness of this method is that an OP that's down, but with whom we already
// created an association in the past will still pass this "are you alive?" test.
Association association = getAssociation(relyingParty, endpointCandidate, true);
if (association != null) {
Logger.DebugFormat("Endpoint #{0} (1-based index) responded to an association request. Selecting that endpoint.", winningEndpointIndex);
// We have a winner!
return endpointCandidate;
}
}
// Since all OPs failed to form an association with us, just return the first endpoint
// and hope for the best.
Logger.Debug("All endpoints failed to respond to an association request. Selecting first endpoint to try to authenticate to.");
return endpoints[0];
}
static Association getAssociation(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, bool createNewAssociationIfNeeded) {
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (provider == null) throw new ArgumentNullException("provider");
// 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.AppendQueryArgs(returnToBuilder, this.ReturnToArgs);
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);
if (this.assoc != null)
qsArgs.Add(protocol.openid.assoc_handle, this.assoc.Handle);
// 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();
}
}
}