diff options
Diffstat (limited to 'src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs')
-rw-r--r-- | src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs | 186 |
1 files changed, 118 insertions, 68 deletions
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs index a3c71ca..a290149 100644 --- a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs @@ -27,19 +27,18 @@ namespace DotNetOpenId.RelyingParty { [DebuggerDisplay("ClaimedIdentifier: {ClaimedIdentifier}, Mode: {Mode}, OpenId: {protocol.Version}")]
class AuthenticationRequest : IAuthenticationRequest {
- Association assoc;
+ internal AssociationPreference associationPreference = AssociationPreference.IfPossible;
ServiceEndpoint endpoint;
Protocol protocol { get { return endpoint.Protocol; } }
internal OpenIdRelyingParty RelyingParty;
- AuthenticationRequest(string token, Association assoc, ServiceEndpoint endpoint,
+ 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.assoc = assoc;
this.endpoint = endpoint;
RelyingParty = relyingParty;
Realm = realm;
@@ -48,11 +47,15 @@ namespace DotNetOpenId.RelyingParty { Mode = AuthenticationRequestMode.Setup;
OutgoingExtensions = ExtensionArgumentsManager.CreateOutgoingExtensions(endpoint.Protocol);
ReturnToArgs = new Dictionary<string, string>();
- if (token != null)
- AddCallbackArguments(DotNetOpenId.RelyingParty.Token.TokenKey, token);
}
- internal static AuthenticationRequest Create(Identifier userSuppliedIdentifier,
- OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl) {
+
+ /// <summary>
+ /// 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.
+ /// </summary>
+ internal static IEnumerable<AuthenticationRequest> 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");
@@ -79,11 +82,6 @@ namespace DotNetOpenId.RelyingParty { }
}
- var endpoints = new List<ServiceEndpoint>(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.
@@ -91,19 +89,88 @@ namespace DotNetOpenId.RelyingParty { 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;
+ // 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);
+ }
+
+ /// <summary>
+ /// Performs request generation for the <see cref="Create"/> method.
+ /// All data validation and cleansing steps must have ALREADY taken place.
+ /// </summary>
+ private static IEnumerable<AuthenticationRequest> CreateInternal(Identifier userSuppliedIdentifier,
+ OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl,
+ IEnumerable<ServiceEndpoint> serviceEndpoints, bool createNewAssociationsAsNeeded) {
+ Logger.InfoFormat("Performing discovery on user-supplied identifier: {0}", userSuppliedIdentifier);
+ IEnumerable<ServiceEndpoint> 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<ServiceEndpoint>(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);
+ }
- return new AuthenticationRequest(
- token, association, 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);
+ }
}
/// <summary>
/// Returns a filtered and sorted list of the available OP endpoints for a discovered Identifier.
/// </summary>
- private static List<ServiceEndpoint> filterAndSortEndpoints(ReadOnlyCollection<ServiceEndpoint> endpoints,
+ private static List<ServiceEndpoint> filterAndSortEndpoints(IEnumerable<ServiceEndpoint> endpoints,
OpenIdRelyingParty relyingParty) {
if (endpoints == null) throw new ArgumentNullException("endpoints");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
@@ -112,10 +179,13 @@ namespace DotNetOpenId.RelyingParty { EndpointSelector versionFilter = ep => ((ServiceEndpoint)ep).Protocol.Version >= Protocol.Lookup(relyingParty.Settings.MinimumRequiredOpenIdVersion).Version;
EndpointSelector hostingSiteFilter = relyingParty.EndpointFilter ?? (ep => true);
- var filteredEndpoints = new List<IXrdsProviderEndpoint>(endpoints.Count);
+ bool anyFilteredOut = false;
+ var filteredEndpoints = new List<IXrdsProviderEndpoint>();
foreach (ServiceEndpoint endpoint in endpoints) {
if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) {
filteredEndpoints.Add(endpoint);
+ } else {
+ anyFilteredOut = true;
}
}
@@ -126,22 +196,12 @@ namespace DotNetOpenId.RelyingParty { foreach (ServiceEndpoint endpoint in filteredEndpoints) {
endpointList.Add(endpoint);
}
- return endpointList;
- }
- /// <summary>
- /// Chooses which provider endpoint is the best one to use.
- /// </summary>
- /// <returns>The best endpoint, or null if no acceptable endpoints were found.</returns>
- private static ServiceEndpoint selectEndpoint(ReadOnlyCollection<ServiceEndpoint> endpoints,
- OpenIdRelyingParty relyingParty) {
-
- List<ServiceEndpoint> filteredEndpoints = filterAndSortEndpoints(endpoints, relyingParty);
- if (filteredEndpoints.Count != endpoints.Count) {
+ if (anyFilteredOut) {
Logger.DebugFormat("Some endpoints were filtered out. Total endpoints remaining: {0}", filteredEndpoints.Count);
}
if (Logger.IsDebugEnabled) {
- if (Util.AreSequencesEquivalent(endpoints, filteredEndpoints)) {
+ 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:");
@@ -149,44 +209,18 @@ namespace DotNetOpenId.RelyingParty { }
}
- // 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];
+ 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).
@@ -282,6 +316,13 @@ namespace DotNetOpenId.RelyingParty { 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<string, string> {
+ { DotNetOpenId.RelyingParty.Token.TokenKey, token },
+ });
+ }
+
var qsArgs = new Dictionary<string, string>();
qsArgs.Add(protocol.openid.mode, (Mode == AuthenticationRequestMode.Immediate) ?
@@ -295,8 +336,17 @@ namespace DotNetOpenId.RelyingParty { 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);
+ 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))
|