summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenId/RelyingParty
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenId/RelyingParty')
-rw-r--r--src/DotNetOpenId/RelyingParty/ApplicationMemoryStore.cs5
-rw-r--r--src/DotNetOpenId/RelyingParty/AssociateRequest.cs66
-rw-r--r--src/DotNetOpenId/RelyingParty/AssociateResponse.cs43
-rw-r--r--src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs223
-rw-r--r--src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs276
-rw-r--r--src/DotNetOpenId/RelyingParty/AuthenticationResponseSnapshot.cs56
-rw-r--r--src/DotNetOpenId/RelyingParty/CheckAuthRequest.cs11
-rw-r--r--src/DotNetOpenId/RelyingParty/CheckAuthResponse.cs4
-rw-r--r--src/DotNetOpenId/RelyingParty/DirectMessageHttpChannel.cs59
-rw-r--r--src/DotNetOpenId/RelyingParty/DirectRequest.cs42
-rw-r--r--src/DotNetOpenId/RelyingParty/DirectResponse.cs34
-rw-r--r--src/DotNetOpenId/RelyingParty/FailedAuthenticationResponse.cs12
-rw-r--r--src/DotNetOpenId/RelyingParty/IAuthenticationRequest.cs48
-rw-r--r--src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs71
-rw-r--r--src/DotNetOpenId/RelyingParty/IDirectMessageChannel.cs7
-rw-r--r--src/DotNetOpenId/RelyingParty/IProviderEndpoint.cs49
-rw-r--r--src/DotNetOpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs22
-rw-r--r--src/DotNetOpenId/RelyingParty/IXrdsProviderEndpoint.cs28
-rw-r--r--src/DotNetOpenId/RelyingParty/IndirectMessageRequest.cs3
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs773
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js455
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdLogin.cs53
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdMobileTextBox.cs2
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs303
-rw-r--r--src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs242
-rw-r--r--src/DotNetOpenId/RelyingParty/RelyingPartySecuritySettings.cs67
-rw-r--r--src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs190
-rw-r--r--src/DotNetOpenId/RelyingParty/Token.cs80
-rw-r--r--src/DotNetOpenId/RelyingParty/login_failure.pngbin0 -> 714 bytes
-rw-r--r--src/DotNetOpenId/RelyingParty/login_success (lock).pngbin0 -> 571 bytes
-rw-r--r--src/DotNetOpenId/RelyingParty/login_success.pngbin0 -> 464 bytes
-rw-r--r--src/DotNetOpenId/RelyingParty/spinner.gifbin0 -> 725 bytes
32 files changed, 2891 insertions, 333 deletions
diff --git a/src/DotNetOpenId/RelyingParty/ApplicationMemoryStore.cs b/src/DotNetOpenId/RelyingParty/ApplicationMemoryStore.cs
index 5559a43..0ff7d46 100644
--- a/src/DotNetOpenId/RelyingParty/ApplicationMemoryStore.cs
+++ b/src/DotNetOpenId/RelyingParty/ApplicationMemoryStore.cs
@@ -14,8 +14,7 @@ namespace DotNetOpenId.RelyingParty {
if (secretSigningKey == null) {
lock (this) {
if (secretSigningKey == null) {
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Generating new secret signing key.");
+ Logger.Info("Generating new secret signing key.");
// initialize in a local variable before setting in field for thread safety.
byte[] auth_key = new byte[64];
new RNGCryptoServiceProvider().GetBytes(auth_key);
@@ -30,8 +29,6 @@ namespace DotNetOpenId.RelyingParty {
List<Nonce> nonces = new List<Nonce>();
public bool TryStoreNonce(Nonce nonce) {
- if (TraceUtil.Switch.TraceVerbose)
- Trace.TraceInformation("Storing nonce: {0}", nonce.Code);
lock (this) {
if (nonces.Contains(nonce)) return false;
nonces.Add(nonce);
diff --git a/src/DotNetOpenId/RelyingParty/AssociateRequest.cs b/src/DotNetOpenId/RelyingParty/AssociateRequest.cs
index 4f6077e..cf3d7f8 100644
--- a/src/DotNetOpenId/RelyingParty/AssociateRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/AssociateRequest.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
-using System.Text;
-using Org.Mentalis.Security.Cryptography;
using System.Diagnostics;
+using System.Globalization;
+using Org.Mentalis.Security.Cryptography;
namespace DotNetOpenId.RelyingParty {
[DebuggerDisplay("Mode: {Args[\"openid.mode\"]}, {Args[\"openid.assoc_type\"]}, OpenId: {Protocol.Version}")]
@@ -10,36 +10,45 @@ namespace DotNetOpenId.RelyingParty {
/// <summary>
/// Instantiates an <see cref="AssociateRequest"/> object.
/// </summary>
+ /// <param name="relyingParty">The RP instance that is creating this request.</param>
/// <param name="provider">The discovered OpenID Provider endpoint information.</param>
/// <param name="args">The arguments assembled for sending to the Provider.</param>
/// <param name="dh">Optional. Supplied only if Diffie-Hellman is used for encrypting the association secret key.</param>
- AssociateRequest(ServiceEndpoint provider, IDictionary<string, string> args, DiffieHellman dh)
- : base(provider, args) {
+ AssociateRequest(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> args, DiffieHellman dh)
+ : base(relyingParty, provider, args) {
DH = dh;
}
public DiffieHellman DH { get; private set; }
- public static AssociateRequest Create(ServiceEndpoint provider) {
- bool useSha256 = provider.Protocol.Version.Major >= 2;
- string assoc_type = useSha256 ?
- provider.Protocol.Args.SignatureAlgorithm.HMAC_SHA256 :
- provider.Protocol.Args.SignatureAlgorithm.HMAC_SHA1;
- string session_type = useSha256 ?
- provider.Protocol.Args.SessionType.DH_SHA256 :
- provider.Protocol.Args.SessionType.DH_SHA1;
- return Create(provider, assoc_type, session_type);
+ public static AssociateRequest Create(OpenIdRelyingParty relyingParty, ServiceEndpoint provider) {
+ if (relyingParty == null) throw new ArgumentNullException("relyingParty");
+ if (provider == null) throw new ArgumentNullException("provider");
+
+ string assoc_type, session_type;
+ bool requireDiffieHellman = !string.Equals(provider.ProviderEndpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
+ if (HmacShaAssociation.TryFindBestAssociation(provider.Protocol,
+ relyingParty.Settings.MinimumHashBitLength, relyingParty.Settings.MaximumHashBitLength,
+ requireDiffieHellman, out assoc_type, out session_type)) {
+ return Create(relyingParty, provider, assoc_type, session_type, true);
+ } else {
+ // There are no associations that meet all requirements.
+ Logger.Warn("Security requirements and protocol combination knock out all possible association types. Dumb mode forced.");
+ return null;
+ }
}
- public static AssociateRequest Create(ServiceEndpoint provider, string assoc_type, string session_type) {
+ public static AssociateRequest Create(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, string assoc_type, string session_type, bool allowNoSession) {
+ if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (provider == null) throw new ArgumentNullException("provider");
if (assoc_type == null) throw new ArgumentNullException("assoc_type");
if (session_type == null) throw new ArgumentNullException("session_type");
Debug.Assert(Array.IndexOf(provider.Protocol.Args.SignatureAlgorithm.All, assoc_type) >= 0);
Debug.Assert(Array.IndexOf(provider.Protocol.Args.SessionType.All, session_type) >= 0);
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Requesting association with {0} (assoc_type = '{1}', session_type = '{2}').",
- provider.ProviderEndpoint, assoc_type, session_type);
+ if (!HmacShaAssociation.IsDHSessionCompatible(provider.Protocol, assoc_type, session_type)) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.IncompatibleAssociationAndSessionTypes, assoc_type, session_type));
+ }
var args = new Dictionary<string, string>();
Protocol protocol = provider.Protocol;
@@ -49,27 +58,32 @@ namespace DotNetOpenId.RelyingParty {
DiffieHellman dh = null;
- if (provider.ProviderEndpoint.Scheme == Uri.UriSchemeHttps) {
+ if (provider.ProviderEndpoint.Scheme == Uri.UriSchemeHttps && allowNoSession) {
+ Logger.InfoFormat("Requesting association with {0} (assoc_type = '{1}', session_type = '{2}').",
+ provider.ProviderEndpoint, assoc_type, protocol.Args.SessionType.NoEncryption);
args.Add(protocol.openid.session_type, protocol.Args.SessionType.NoEncryption);
} else {
+ Logger.InfoFormat("Requesting association with {0} (assoc_type = '{1}', session_type = '{2}').",
+ provider.ProviderEndpoint, assoc_type, session_type);
+
// Initiate Diffie-Hellman Exchange
- dh = CryptUtil.CreateDiffieHellman();
+ dh = DiffieHellmanUtil.CreateDiffieHellman();
byte[] dhPublic = dh.CreateKeyExchange();
- string cpub = CryptUtil.UnsignedToBase64(dhPublic);
+ string cpub = DiffieHellmanUtil.UnsignedToBase64(dhPublic);
args.Add(protocol.openid.session_type, session_type);
args.Add(protocol.openid.dh_consumer_public, cpub);
DHParameters dhps = dh.ExportParameters(true);
- if (dhps.P != CryptUtil.DEFAULT_MOD || dhps.G != CryptUtil.DEFAULT_GEN) {
- args.Add(protocol.openid.dh_modulus, CryptUtil.UnsignedToBase64(dhps.P));
- args.Add(protocol.openid.dh_gen, CryptUtil.UnsignedToBase64(dhps.G));
+ if (dhps.P != DiffieHellmanUtil.DEFAULT_MOD || dhps.G != DiffieHellmanUtil.DEFAULT_GEN) {
+ args.Add(protocol.openid.dh_modulus, DiffieHellmanUtil.UnsignedToBase64(dhps.P));
+ args.Add(protocol.openid.dh_gen, DiffieHellmanUtil.UnsignedToBase64(dhps.G));
}
}
- return new AssociateRequest(provider, args, dh);
+ return new AssociateRequest(relyingParty, provider, args, dh);
}
AssociateResponse response;
[DebuggerBrowsable(DebuggerBrowsableState.Never)] // code execution in getter
@@ -77,10 +91,10 @@ namespace DotNetOpenId.RelyingParty {
get {
if (response == null) {
try {
- response = new AssociateResponse(Provider, GetResponse(), DH);
+ response = new AssociateResponse(RelyingParty, Provider, GetResponse(), DH);
} catch (OpenIdException ex) {
if (ex.Query != null) {
- response = new AssociateResponse(Provider, ex.Query, DH);
+ response = new AssociateResponse(RelyingParty, Provider, ex.Query, DH);
}
// Silently fail at associate attempt, since we can recover
// using dumb mode.
diff --git a/src/DotNetOpenId/RelyingParty/AssociateResponse.cs b/src/DotNetOpenId/RelyingParty/AssociateResponse.cs
index 8543d24..c555f8c 100644
--- a/src/DotNetOpenId/RelyingParty/AssociateResponse.cs
+++ b/src/DotNetOpenId/RelyingParty/AssociateResponse.cs
@@ -7,8 +7,8 @@ using System.Diagnostics;
namespace DotNetOpenId.RelyingParty {
class AssociateResponse : DirectResponse {
- public AssociateResponse(ServiceEndpoint provider, IDictionary<string, string> args, DiffieHellman dh)
- : base(provider, args) {
+ public AssociateResponse(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> args, DiffieHellman dh)
+ : base(relyingParty, provider, args) {
DH = dh;
if (Args.ContainsKey(Protocol.openidnp.assoc_handle)) {
@@ -21,8 +21,9 @@ namespace DotNetOpenId.RelyingParty {
string session_type = Util.GetRequiredArg(Args, Protocol.openidnp.session_type);
// If the suggested options are among those we support...
if (Array.IndexOf(Protocol.Args.SignatureAlgorithm.All, assoc_type) >= 0 &&
- Array.IndexOf(Protocol.Args.SessionType.All, session_type) >= 0) {
- SecondAttempt = AssociateRequest.Create(Provider, assoc_type, session_type);
+ Array.IndexOf(Protocol.Args.SessionType.All, session_type) >= 0 &&
+ RelyingParty.Settings.IsAssociationInPermittedRange(Protocol, assoc_type)) {
+ SecondAttempt = AssociateRequest.Create(RelyingParty, Provider, assoc_type, session_type, false);
}
}
}
@@ -31,39 +32,35 @@ namespace DotNetOpenId.RelyingParty {
void initializeAssociation() {
string assoc_type = Util.GetRequiredArg(Args, Protocol.openidnp.assoc_type);
- if (Protocol.Args.SignatureAlgorithm.HMAC_SHA1.Equals(assoc_type, StringComparison.Ordinal) ||
- Protocol.Args.SignatureAlgorithm.HMAC_SHA256.Equals(assoc_type, StringComparison.Ordinal)) {
+ if (Array.IndexOf(Protocol.Args.SignatureAlgorithm.All, assoc_type) >= 0) {
byte[] secret;
string session_type;
if (!Args.TryGetValue(Protocol.openidnp.session_type, out session_type) ||
Protocol.Args.SessionType.NoEncryption.Equals(session_type, StringComparison.Ordinal)) {
secret = getDecoded(Protocol.openidnp.mac_key);
- } else if (Protocol.Args.SessionType.DH_SHA1.Equals(session_type, StringComparison.Ordinal)) {
- byte[] dh_server_public = getDecoded(Protocol.openidnp.dh_server_public);
- byte[] enc_mac_key = getDecoded(Protocol.openidnp.enc_mac_key);
- secret = CryptUtil.SHAHashXorSecret(CryptUtil.Sha1, DH, dh_server_public, enc_mac_key);
- } else if (Protocol.Args.SessionType.DH_SHA256.Equals(session_type, StringComparison.Ordinal)) {
- byte[] dh_server_public = getDecoded(Protocol.openidnp.dh_server_public);
- byte[] enc_mac_key = getDecoded(Protocol.openidnp.enc_mac_key);
- secret = CryptUtil.SHAHashXorSecret(CryptUtil.Sha256, DH, dh_server_public, enc_mac_key);
} else {
- throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- Strings.InvalidOpenIdQueryParameterValue,
- Protocol.openid.session_type, session_type));
+ try {
+ byte[] dh_server_public = getDecoded(Protocol.openidnp.dh_server_public);
+ byte[] enc_mac_key = getDecoded(Protocol.openidnp.enc_mac_key);
+ secret = DiffieHellmanUtil.SHAHashXorSecret(DiffieHellmanUtil.Lookup(Protocol, session_type), DH, dh_server_public, enc_mac_key);
+ } catch (ArgumentException ex) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.InvalidOpenIdQueryParameterValue,
+ Protocol.openid.session_type, session_type), ex);
+ }
}
string assocHandle = Util.GetRequiredArg(Args, Protocol.openidnp.assoc_handle);
TimeSpan expiresIn = new TimeSpan(0, 0, Convert.ToInt32(Util.GetRequiredArg(Args, Protocol.openidnp.expires_in), CultureInfo.InvariantCulture));
- if (assoc_type == Protocol.Args.SignatureAlgorithm.HMAC_SHA1) {
- Association = new HmacSha1Association(assocHandle, secret, expiresIn);
- } else if (assoc_type == Protocol.Args.SignatureAlgorithm.HMAC_SHA256) {
- Association = new HmacSha256Association(assocHandle, secret, expiresIn);
- } else {
+ try {
+ Association = HmacShaAssociation.Create(Protocol, assoc_type,
+ assocHandle, secret, expiresIn);
+ } catch (ArgumentException ex) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue,
- Protocol.openid.assoc_type, assoc_type));
+ Protocol.openid.assoc_type, assoc_type), ex);
}
} else {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
index cc9c3ae..a3c71ca 100644
--- a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs
@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
-using System.Text;
-using DotNetOpenId;
+using System.Collections.ObjectModel;
using System.Collections.Specialized;
+using System.Diagnostics;
using System.Globalization;
using System.Web;
-using System.Diagnostics;
namespace DotNetOpenId.RelyingParty {
/// <summary>
@@ -30,18 +29,19 @@ namespace DotNetOpenId.RelyingParty {
class AuthenticationRequest : IAuthenticationRequest {
Association assoc;
ServiceEndpoint endpoint;
- MessageEncoder encoder;
Protocol protocol { get { return endpoint.Protocol; } }
+ internal OpenIdRelyingParty RelyingParty;
AuthenticationRequest(string token, Association assoc, ServiceEndpoint endpoint,
- Realm realm, Uri returnToUrl, MessageEncoder encoder) {
+ 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 (encoder == null) throw new ArgumentNullException("encoder");
+ if (relyingParty == null) throw new ArgumentNullException("relyingParty");
+
this.assoc = assoc;
this.endpoint = endpoint;
- this.encoder = encoder;
+ RelyingParty = relyingParty;
Realm = realm;
ReturnToUrl = returnToUrl;
@@ -52,38 +52,37 @@ namespace DotNetOpenId.RelyingParty {
AddCallbackArguments(DotNetOpenId.RelyingParty.Token.TokenKey, token);
}
internal static AuthenticationRequest Create(Identifier userSuppliedIdentifier,
- Realm realm, Uri returnToUrl, IRelyingPartyApplicationStore store, MessageEncoder encoder) {
+ 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");
- if (TraceUtil.Switch.TraceInfo) {
- Trace.TraceInformation("Creating authentication request for user supplied Identifier: {0}",
- userSuppliedIdentifier);
+ 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);
}
- if (TraceUtil.Switch.TraceVerbose) {
- Trace.Indent();
- Trace.TraceInformation("Realm: {0}", realm);
- Trace.TraceInformation("Return To: {0}", returnToUrl);
- Trace.Unindent();
- }
- if (TraceUtil.Switch.TraceWarning && returnToUrl.Query != null) {
+ 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)) {
- Trace.TraceWarning("OpenId argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key);
+ Logger.WarnFormat("OpenId argument \"{0}\" found in return_to URL. This can corrupt an OpenID response.", key);
break;
}
}
}
- var endpoint = userSuppliedIdentifier.Discover();
+ var endpoints = new List<ServiceEndpoint>(userSuppliedIdentifier.Discover());
+ ServiceEndpoint endpoint = selectEndpoint(endpoints.AsReadOnly(), relyingParty);
if (endpoint == null)
throw new OpenIdException(Strings.OpenIdEndpointNotFound);
- if (TraceUtil.Switch.TraceVerbose) {
- Trace.Indent();
- Trace.TraceInformation("Discovered provider endpoint: {0}", endpoint);
- Trace.Unindent();
- }
// 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
@@ -92,35 +91,151 @@ 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;
+
return new AuthenticationRequest(
- new Token(endpoint).Serialize(store),
- store != null ? getAssociation(endpoint, store) : null,
- endpoint, realm, returnToUrl, encoder);
+ token, association, endpoint, realm, returnToUrl, relyingParty);
+ }
+
+ /// <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,
+ 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<IXrdsProviderEndpoint>(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<ServiceEndpoint> endpointList = new List<ServiceEndpoint>(filteredEndpoints.Count);
+ foreach (ServiceEndpoint endpoint in filteredEndpoints) {
+ endpointList.Add(endpoint);
+ }
+ return endpointList;
}
- static Association getAssociation(ServiceEndpoint provider, IRelyingPartyApplicationStore store) {
+
+ /// <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) {
+ 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");
- if (store == null) throw new ArgumentNullException("store");
- Association assoc = store.GetAssociation(provider.ProviderEndpoint);
+ // 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) {
- var req = AssociateRequest.Create(provider);
+ 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) {
- if (TraceUtil.Switch.TraceWarning) {
- Trace.TraceWarning("Initial association attempt failed, but will retry with Provider-suggested parameters.");
- }
+ 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) {
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Association with {0} established.", provider.ProviderEndpoint);
- store.StoreAssociation(provider.ProviderEndpoint, assoc);
+ Logger.InfoFormat("Association with {0} established.", provider.ProviderEndpoint);
+ relyingParty.Store.StoreAssociation(provider.ProviderEndpoint, assoc);
} else {
- if (TraceUtil.Switch.TraceError) {
- Trace.TraceError("Association attempt with {0} provider failed.", provider);
- }
+ Logger.ErrorFormat("Association attempt with {0} provider failed.", provider.ProviderEndpoint);
}
}
}
@@ -141,19 +256,31 @@ namespace DotNetOpenId.RelyingParty {
public AuthenticationRequestMode Mode { get; set; }
public Realm Realm { get; private set; }
public Uri ReturnToUrl { get; private set; }
- public Identifier ClaimedIdentifier { get { return endpoint.ClaimedIdentifier; } }
+ public Identifier ClaimedIdentifier {
+ get { return IsDirectedIdentity ? null : endpoint.ClaimedIdentifier; }
+ }
+ public bool IsDirectedIdentity {
+ get { return endpoint.ClaimedIdentifier == endpoint.Protocol.ClaimedIdentifierForOPIdentifier; }
+ }
/// <summary>
/// The detected version of OpenID implemented by the Provider.
/// </summary>
public Version ProviderVersion { get { return protocol.Version; } }
/// <summary>
- /// Gets the URL the user agent should be redirected to to begin the
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ IProviderEndpoint IAuthenticationRequest.Provider { get { return endpoint; } }
+
+ /// <summary>
+ /// Gets the response to send to the user agent to begin the
/// OpenID authentication process.
/// </summary>
public IResponse RedirectingResponse {
get {
UriBuilder returnToBuilder = new UriBuilder(ReturnToUrl);
- UriUtil.AppendQueryArgs(returnToBuilder, this.ReturnToArgs);
+ UriUtil.AppendAndReplaceQueryArgs(returnToBuilder, this.ReturnToArgs);
var qsArgs = new Dictionary<string, string>();
@@ -172,11 +299,11 @@ namespace DotNetOpenId.RelyingParty {
qsArgs.Add(protocol.openid.assoc_handle, this.assoc.Handle);
// Add on extension arguments
- foreach(var pair in OutgoingExtensions.GetArgumentsToSend(true))
+ foreach (var pair in OutgoingExtensions.GetArgumentsToSend(true))
qsArgs.Add(pair.Key, pair.Value);
var request = new IndirectMessageRequest(this.endpoint.ProviderEndpoint, qsArgs);
- return this.encoder.Encode(request);
+ return RelyingParty.Encoder.Encode(request);
}
}
@@ -214,7 +341,7 @@ namespace DotNetOpenId.RelyingParty {
/// This method requires an ASP.NET HttpContext.
/// </remarks>
public void RedirectToProvider() {
- if (HttpContext.Current == null || HttpContext.Current.Response == null)
+ if (HttpContext.Current == null || HttpContext.Current.Response == null)
throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
RedirectingResponse.Send();
}
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs
index 6942cd5..6766304 100644
--- a/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs
+++ b/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs
@@ -21,9 +21,12 @@ namespace DotNetOpenId.RelyingParty {
/// </summary>
Failed,
/// <summary>
- /// The Provider responded to a request for immediate authentication approval
+ /// <para>The Provider responded to a request for immediate authentication approval
/// with a message stating that additional user agent interaction is required
- /// before authentication can be completed.
+ /// before authentication can be completed.</para>
+ /// <para>Casting the <see cref="IAuthenticationResponse"/> to a
+ /// <see cref="ISetupRequiredAuthenticationResponse"/> in this case can help
+ /// you retry the authentication using setup (non-immediate) mode.</para>
/// </summary>
SetupRequired,
/// <summary>
@@ -33,10 +36,19 @@ namespace DotNetOpenId.RelyingParty {
}
[DebuggerDisplay("Status: {Status}, ClaimedIdentifier: {ClaimedIdentifier}")]
- class AuthenticationResponse : IAuthenticationResponse {
+ class AuthenticationResponse : IAuthenticationResponse, ISetupRequiredAuthenticationResponse {
internal AuthenticationResponse(AuthenticationStatus status, ServiceEndpoint provider, IDictionary<string, string> query) {
if (provider == null) throw new ArgumentNullException("provider");
if (query == null) throw new ArgumentNullException("query");
+
+ if (status == AuthenticationStatus.Authenticated) {
+ Logger.InfoFormat("Verified positive authentication assertion for: {0}", provider.ClaimedIdentifier);
+ } else {
+ Logger.InfoFormat("Negative authentication assertion received: {0}", status);
+ }
+
+ // TODO: verify signature on callback args
+ CallbackArguments = cleanQueryForCallbackArguments(query);
Status = status;
Provider = provider;
signedArguments = new Dictionary<string, string>();
@@ -53,6 +65,26 @@ namespace DotNetOpenId.RelyingParty {
IncomingExtensions = ExtensionArgumentsManager.CreateIncomingExtensions(signedArguments);
}
+ internal IDictionary<string, string> CallbackArguments;
+ public IDictionary<string, string> GetCallbackArguments() {
+ // Return a copy so that the caller cannot change the contents.
+ return new Dictionary<string, string>(CallbackArguments);
+ }
+ /// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <returns>The value of the argument, or null if the named parameter could not be found.</returns>
+ public string GetCallbackArgument(string key) {
+ if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
+
+ string value;
+ if (CallbackArguments.TryGetValue(key, out value)) {
+ return value;
+ }
+ return null;
+ }
+
/// <summary>
/// The detailed success or failure status of the authentication attempt.
/// </summary>
@@ -67,8 +99,24 @@ namespace DotNetOpenId.RelyingParty {
/// An Identifier that the end user claims to own.
/// </summary>
public Identifier ClaimedIdentifier {
- get { return Provider.ClaimedIdentifier; }
+ get {
+ if (Provider.ClaimedIdentifier == Provider.Protocol.ClaimedIdentifierForOPIdentifier) {
+ return null; // no claimed identifier -- failed directed identity authentication
+ }
+ return Provider.ClaimedIdentifier;
+ }
+ }
+ /// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <remarks>
+ /// See <see cref="IAuthenticationResponse.FriendlyIdentifierForDisplay"/>.
+ /// </remarks>
+ public string FriendlyIdentifierForDisplay {
+ [DebuggerStepThrough]
+ get { return Provider.FriendlyIdentifierForDisplay; }
}
+
/// <summary>
/// The discovered endpoint information.
/// </summary>
@@ -86,6 +134,56 @@ namespace DotNetOpenId.RelyingParty {
get { return new Uri(Util.GetRequiredArg(signedArguments, Provider.Protocol.openid.return_to)); }
}
+ internal string GetExtensionClientScript(Type extensionType) {
+ if (extensionType == null) throw new ArgumentNullException("extensionType");
+ if (!typeof(DotNetOpenId.Extensions.IClientScriptExtensionResponse).IsAssignableFrom(extensionType))
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.TypeMustImplementX, typeof(IClientScriptExtensionResponse).FullName),
+ "extensionType");
+ var extension = (IClientScriptExtensionResponse)Activator.CreateInstance(extensionType);
+ return GetExtensionClientScript(extension);
+ }
+
+ internal string GetExtensionClientScript(IClientScriptExtensionResponse extension) {
+ var fields = IncomingExtensions.GetExtensionArguments(extension.TypeUri);
+ if (fields != null) {
+ // The extension was found using the preferred TypeUri.
+ return extension.InitializeJavaScriptData(fields, this, extension.TypeUri);
+ } else {
+ // The extension may still be found using secondary TypeUris.
+ if (extension.AdditionalSupportedTypeUris != null) {
+ foreach (string typeUri in extension.AdditionalSupportedTypeUris) {
+ fields = IncomingExtensions.GetExtensionArguments(typeUri);
+ if (fields != null) {
+ // We found one of the older ones.
+ return extension.InitializeJavaScriptData(fields, this, typeUri);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ bool getExtension(IExtensionResponse extension) {
+ var fields = IncomingExtensions.GetExtensionArguments(extension.TypeUri);
+ if (fields != null) {
+ // The extension was found using the preferred TypeUri.
+ return extension.Deserialize(fields, this, extension.TypeUri);
+ } else {
+ // The extension may still be found using secondary TypeUris.
+ if (extension.AdditionalSupportedTypeUris != null) {
+ foreach (string typeUri in extension.AdditionalSupportedTypeUris) {
+ fields = IncomingExtensions.GetExtensionArguments(typeUri);
+ if (fields != null) {
+ // We found one of the older ones.
+ return extension.Deserialize(fields, this, typeUri);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
/// <summary>
/// Tries to get an OpenID extension that may be present in the response.
/// </summary>
@@ -93,7 +191,7 @@ namespace DotNetOpenId.RelyingParty {
/// <returns>The extension, if it is found. Null otherwise.</returns>
public T GetExtension<T>() where T : IExtensionResponse, new() {
T extension = new T();
- return extension.Deserialize(IncomingExtensions.GetExtensionArguments(extension.TypeUri), this) ? extension : default(T);
+ return getExtension(extension) ? extension : default(T);
}
public IExtensionResponse GetExtension(Type extensionType) {
@@ -103,13 +201,16 @@ namespace DotNetOpenId.RelyingParty {
Strings.TypeMustImplementX, typeof(IExtensionResponse).FullName),
"extensionType");
var extension = (IExtensionResponse)Activator.CreateInstance(extensionType);
- return extension.Deserialize(IncomingExtensions.GetExtensionArguments(extension.TypeUri), this) ? extension : null;
+ return getExtension(extension) ? extension : null;
}
internal static AuthenticationResponse Parse(IDictionary<string, string> query,
- IRelyingPartyApplicationStore store, Uri requestUrl) {
+ OpenIdRelyingParty relyingParty, Uri requestUrl, bool verifySignature) {
if (query == null) throw new ArgumentNullException("query");
if (requestUrl == null) throw new ArgumentNullException("requestUrl");
+
+ Logger.DebugFormat("OpenID authentication response received:{0}{1}", Environment.NewLine, Util.ToString(query));
+
ServiceEndpoint tokenEndpoint = null;
// The query parameter may be the POST query or the GET query,
// but the token parameter will always be in the GET query because
@@ -118,7 +219,8 @@ namespace DotNetOpenId.RelyingParty {
HttpUtility.ParseQueryString(requestUrl.Query));
string token = Util.GetOptionalArg(requestUrlQuery, Token.TokenKey);
if (token != null) {
- tokenEndpoint = Token.Deserialize(token, store).Endpoint;
+ token = FixDoublyUriDecodedToken(token);
+ tokenEndpoint = Token.Deserialize(token, relyingParty.Store).Endpoint;
}
Protocol protocol = Protocol.Detect(query);
@@ -139,8 +241,11 @@ namespace DotNetOpenId.RelyingParty {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.MissingInternalQueryParameter, Token.TokenKey));
} else {
- // 2.0 OPs provide enough information to assemble the entire endpoint info
- responseEndpoint = ServiceEndpoint.ParseFromAuthResponse(query);
+ // 2.0 OPs provide enough information to assemble the entire endpoint info,
+ // except perhaps for the original user supplied identifier, which if available
+ // allows us to display a friendly XRI.
+ Identifier friendlyIdentifier = tokenEndpoint != null ? tokenEndpoint.UserSuppliedIdentifier : null;
+ responseEndpoint = ServiceEndpoint.ParseFromAuthResponse(query, friendlyIdentifier);
// If this is a solicited assertion, we'll have a token with endpoint data too,
// which we can use to more quickly confirm the validity of the claimed
// endpoint info.
@@ -151,7 +256,7 @@ namespace DotNetOpenId.RelyingParty {
// verified.
// For the error-handling and cancellation cases, the info does not have to
// be verified, so we'll use whichever one is available.
- return parseIdResResponse(query, tokenEndpoint, responseEndpoint, store, requestUrl);
+ return parseIdResResponse(query, tokenEndpoint, responseEndpoint, relyingParty, requestUrl, verifySignature);
} else {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue,
@@ -159,9 +264,38 @@ namespace DotNetOpenId.RelyingParty {
}
}
+ /// <summary>
+ /// Corrects any URI decoding the Provider may have inappropriately done
+ /// to our return_to URL, resulting in an otherwise corrupted base64 token.
+ /// </summary>
+ /// <param name="token">The token, which MAY have been corrupted by an extra URI decode.</param>
+ /// <returns>The token; corrected if corruption had occurred.</returns>
+ /// <remarks>
+ /// AOL may have incorrectly URI-decoded the token for us in the return_to,
+ /// resulting in a token URI-decoded twice by the time we see it, and no
+ /// longer being a valid base64 string.
+ /// It turns out that the only symbols from base64 that is also encoded
+ /// in URI encoding rules are the + and / characters.
+ /// AOL decodes the %2b sequence to the + character
+ /// and the %2f sequence to the / character (it shouldn't decode at all).
+ /// When we do our own URI decoding, the + character becomes a space (corrupting base64)
+ /// but the / character remains a /, so no further corruption happens to this character.
+ /// So to correct this we just need to change any spaces we find in the token
+ /// back to + characters.
+ /// </remarks>
+ private static string FixDoublyUriDecodedToken(string token) {
+ if (token == null) throw new ArgumentNullException("token");
+ if (token.Contains(" ")) {
+ Logger.Error("Deserializing a corrupted token. The OpenID Provider may have inappropriately decoded the return_to URL before sending it back to us.");
+ token = token.Replace(' ', '+'); // Undo any extra decoding the Provider did
+ }
+
+ return token;
+ }
+
static AuthenticationResponse parseIdResResponse(IDictionary<string, string> query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint,
- IRelyingPartyApplicationStore store, Uri requestUrl) {
+ OpenIdRelyingParty relyingParty, Uri requestUrl, bool verifyMessageSignature) {
// Use responseEndpoint if it is available so we get the
// Claimed Identifer correct in the AuthenticationResponse.
ServiceEndpoint unverifiedEndpoint = responseEndpoint ?? tokenEndpoint;
@@ -173,9 +307,11 @@ namespace DotNetOpenId.RelyingParty {
}
verifyReturnTo(query, unverifiedEndpoint, requestUrl);
- verifyDiscoveredInfoMatchesAssertedInfo(query, tokenEndpoint, responseEndpoint);
- verifyNonceUnused(query, unverifiedEndpoint, store);
- verifySignature(query, unverifiedEndpoint, store);
+ verifyDiscoveredInfoMatchesAssertedInfo(relyingParty, query, tokenEndpoint, responseEndpoint);
+ if (verifyMessageSignature) {
+ verifyNonceUnused(query, unverifiedEndpoint, relyingParty.Store);
+ verifySignature(relyingParty, query, unverifiedEndpoint);
+ }
return new AuthenticationResponse(AuthenticationStatus.Authenticated, unverifiedEndpoint, query);
}
@@ -195,27 +331,49 @@ namespace DotNetOpenId.RelyingParty {
Debug.Assert(endpoint != null);
Debug.Assert(requestUrl != null);
+ Logger.Debug("Verifying return_to...");
Uri return_to = new Uri(Util.GetRequiredArg(query, endpoint.Protocol.openid.return_to));
if (return_to.Scheme != requestUrl.Scheme ||
return_to.Authority != requestUrl.Authority ||
return_to.AbsolutePath != requestUrl.AbsolutePath)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- Strings.ReturnToParamDoesNotMatchRequestUrl, endpoint.Protocol.openid.return_to));
+ Strings.ReturnToParamDoesNotMatchRequestUrl, endpoint.Protocol.openid.return_to,
+ return_to, requestUrl));
NameValueCollection returnToArgs = HttpUtility.ParseQueryString(return_to.Query);
NameValueCollection requestArgs = HttpUtility.ParseQueryString(requestUrl.Query);
foreach (string paramName in returnToArgs) {
if (requestArgs[paramName] != returnToArgs[paramName])
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- Strings.ReturnToParamDoesNotMatchRequestUrl, endpoint.Protocol.openid.return_to));
+ Strings.ReturnToParamDoesNotMatchRequestUrl, endpoint.Protocol.openid.return_to,
+ return_to, requestUrl));
}
}
/// <remarks>
/// This is documented in OpenId Authentication 2.0 section 11.2.
/// </remarks>
- static void verifyDiscoveredInfoMatchesAssertedInfo(IDictionary<string, string> query,
+ static void verifyDiscoveredInfoMatchesAssertedInfo(OpenIdRelyingParty relyingParty,
+ IDictionary<string, string> query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint) {
+
+ Logger.Debug("Verifying assertion matches identifier discovery results...");
+
+ // Verify that the actual version of the OP endpoint matches discovery.
+ Protocol actualProtocol = Protocol.Detect(query);
+ Protocol discoveredProtocol = (tokenEndpoint ?? responseEndpoint).Protocol;
+ if (!actualProtocol.Equals(discoveredProtocol)) {
+ // Allow an exception so that v1.1 and v1.0 can be seen as identical for this
+ // verification. v1.0 has no spec, and v1.1 and v1.0 cannot be clearly distinguished
+ // from the protocol, so detecting their differences is meaningless, and throwing here
+ // would just break thing unnecessarily.
+ if (!(actualProtocol.Version.Major == 1 && discoveredProtocol.Version.Major == 1)) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.OpenIdDiscoveredAndActualVersionMismatch,
+ actualProtocol.Version, discoveredProtocol.Version));
+ }
+ }
+
if ((tokenEndpoint ?? responseEndpoint).Protocol.Version.Major < 2) {
Debug.Assert(tokenEndpoint != null, "Our OpenID 1.x implementation requires an RP token. And this should have been verified by our caller.");
// For 1.x OPs, we only need to verify that the OP Local Identifier
@@ -229,21 +387,28 @@ namespace DotNetOpenId.RelyingParty {
}
} else {
// In 2.0, we definitely have a responseEndpoint, but may not have a
- // tokenEndpoint. If we don't have a tokenEndpoint or if the user
- // gave us an OP Identifier originally, we need to perform discovery on
+ // tokenEndpoint. If we don't have a tokenEndpoint, or it doesn't match the assertion,
+ // or if the user gave us an OP Identifier originally, then we need to perform discovery on
// the responseEndpoint.ClaimedIdentifier to verify the OP has authority
// to speak for it.
- if (tokenEndpoint == null ||
- tokenEndpoint.ClaimedIdentifier == ((Identifier)tokenEndpoint.Protocol.ClaimedIdentifierForOPIdentifier)) {
+ if (tokenEndpoint == null || // no token included (unsolicited assertion)
+ tokenEndpoint != responseEndpoint || // the OP is asserting something different than we asked for
+ tokenEndpoint.ClaimedIdentifier == ((Identifier)tokenEndpoint.Protocol.ClaimedIdentifierForOPIdentifier)) { // or directed identity is in effect
Identifier claimedIdentifier = Util.GetRequiredArg(query, responseEndpoint.Protocol.openid.claimed_id);
- ServiceEndpoint claimedEndpoint = claimedIdentifier.Discover();
- // Compare the two ServiceEndpoints to make sure they are the same.
- if (responseEndpoint != claimedEndpoint)
- throw new OpenIdException(Strings.IssuedAssertionFailsIdentifierDiscovery);
- } else {
- // Check that the assertion matches the service endpoint we know about.
- if (responseEndpoint != tokenEndpoint)
- throw new OpenIdException(Strings.IssuedAssertionFailsIdentifierDiscovery);
+ // Require SSL where appropriate. This will filter out insecure identifiers,
+ // redirects and provider endpoints automatically. If we find a match after all that
+ // filtering with the responseEndpoint, then the unsolicited assertion is secure.
+ if (relyingParty.Settings.RequireSsl && !claimedIdentifier.TryRequireSsl(out claimedIdentifier)) {
+ throw new OpenIdException(Strings.InsecureWebRequestWithSslRequired, query);
+ }
+ Logger.InfoFormat("Provider asserted an identifier that requires (re)discovery to confirm.");
+ List<ServiceEndpoint> discoveredEndpoints = new List<ServiceEndpoint>(claimedIdentifier.Discover());
+ // Make sure the response endpoint matches one of the discovered endpoints.
+ if (!discoveredEndpoints.Contains(responseEndpoint)) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.IssuedAssertionFailsIdentifierDiscovery,
+ responseEndpoint, Util.ToString(discoveredEndpoints)));
+ }
}
}
}
@@ -251,11 +416,13 @@ namespace DotNetOpenId.RelyingParty {
static void verifyNonceUnused(IDictionary<string, string> query, ServiceEndpoint endpoint, IRelyingPartyApplicationStore store) {
if (endpoint.Protocol.Version.Major < 2) return; // nothing to validate
if (store == null) return; // we'll pass verifying the nonce responsibility to the OP
+
+ Logger.Debug("Verifying nonce is unused...");
var nonce = new Nonce(Util.GetRequiredArg(query, endpoint.Protocol.openid.response_nonce), true);
nonce.Consume(store);
}
- static void verifySignature(IDictionary<string, string> query, ServiceEndpoint endpoint, IRelyingPartyApplicationStore store) {
+ static void verifySignature(OpenIdRelyingParty relyingParty, IDictionary<string, string> query, ServiceEndpoint endpoint) {
string signed = Util.GetRequiredArg(query, endpoint.Protocol.openid.signed);
string[] signedFields = signed.Split(',');
@@ -278,17 +445,19 @@ namespace DotNetOpenId.RelyingParty {
// Now actually validate the signature itself.
string assoc_handle = Util.GetRequiredArg(query, endpoint.Protocol.openid.assoc_handle);
- Association assoc = store != null ? store.GetAssociation(endpoint.ProviderEndpoint, assoc_handle) : null;
+ Association assoc = relyingParty.Store != null ? relyingParty.Store.GetAssociation(endpoint.ProviderEndpoint, assoc_handle) : null;
if (assoc == null) {
// It's not an association we know about. Dumb mode is our
// only possible path for recovery.
- verifySignatureByProvider(query, endpoint, store);
+ Logger.Debug("Passing signature back to Provider for verification (no association available)...");
+ verifySignatureByProvider(relyingParty, query, endpoint);
} else {
if (assoc.IsExpired)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
"Association with {0} expired", endpoint.ProviderEndpoint), endpoint.ClaimedIdentifier);
+ Logger.Debug("Verifying signature by association...");
verifySignatureByAssociation(query, endpoint.Protocol, signedFields, assoc);
}
}
@@ -325,12 +494,45 @@ namespace DotNetOpenId.RelyingParty {
/// to the consumer site with an authenticated status.
/// </summary>
/// <returns>Whether the authentication is valid.</returns>
- static void verifySignatureByProvider(IDictionary<string, string> query, ServiceEndpoint provider, IRelyingPartyApplicationStore store) {
- var request = CheckAuthRequest.Create(provider, query);
- if (request.Response.InvalidatedAssociationHandle != null && store != null)
- store.RemoveAssociation(provider.ProviderEndpoint, request.Response.InvalidatedAssociationHandle);
+ static void verifySignatureByProvider(OpenIdRelyingParty relyingParty, IDictionary<string, string> query, ServiceEndpoint provider) {
+ var request = CheckAuthRequest.Create(relyingParty, provider, query);
+ if (request.Response.InvalidatedAssociationHandle != null && relyingParty.Store != null)
+ relyingParty.Store.RemoveAssociation(provider.ProviderEndpoint, request.Response.InvalidatedAssociationHandle);
if (!request.Response.IsAuthenticationValid)
throw new OpenIdException(Strings.InvalidSignature);
}
+
+ static IDictionary<string, string> cleanQueryForCallbackArguments(IDictionary<string, string> query) {
+ var dictionary = new Dictionary<string, string>();
+ foreach (var pair in query) {
+ // Disallow lookup of any openid parameters.
+ if (pair.Key.StartsWith("openid.", StringComparison.OrdinalIgnoreCase)) {
+ continue;
+ }
+ dictionary.Add(pair.Key, pair.Value);
+ }
+ return dictionary;
+ }
+
+ #region ISetupRequiredAuthenticationResponse Members
+
+ /// <summary>
+ /// The <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/>
+ /// in a subsequent authentication attempt.
+ /// </summary>
+ /// <remarks>
+ /// When directed identity is used, this will be the Provider Identifier given by the user.
+ /// Otherwise it will be the Claimed Identifier derived from the user-supplied identifier.
+ /// </remarks>
+ public Identifier ClaimedOrProviderIdentifier {
+ get {
+ if (Status != AuthenticationStatus.SetupRequired) {
+ throw new InvalidOperationException(Strings.OperationOnlyValidForSetupRequiredState);
+ }
+ return ClaimedIdentifier ?? Provider.UserSuppliedIdentifier;
+ }
+ }
+
+ #endregion
}
}
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationResponseSnapshot.cs b/src/DotNetOpenId/RelyingParty/AuthenticationResponseSnapshot.cs
new file mode 100644
index 0000000..7026aaa
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/AuthenticationResponseSnapshot.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.RelyingParty {
+ [Serializable]
+ class AuthenticationResponseSnapshot : IAuthenticationResponse {
+ internal AuthenticationResponseSnapshot(IAuthenticationResponse copyFrom) {
+ if (copyFrom == null) throw new ArgumentNullException("copyFrom");
+
+ ClaimedIdentifier = copyFrom.ClaimedIdentifier;
+ FriendlyIdentifierForDisplay = copyFrom.FriendlyIdentifierForDisplay;
+ Status = copyFrom.Status;
+ callbackArguments = copyFrom.GetCallbackArguments();
+ }
+
+ IDictionary<string, string> callbackArguments;
+
+ #region IAuthenticationResponse Members
+
+ public IDictionary<string, string> GetCallbackArguments() {
+ // Return a copy so that the caller cannot change the contents.
+ return new Dictionary<string, string>(callbackArguments);
+ }
+
+ public string GetCallbackArgument(string key) {
+ if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
+
+ string value;
+ if (callbackArguments.TryGetValue(key, out value)) {
+ return value;
+ }
+ return null;
+ }
+
+ public T GetExtension<T>() where T : DotNetOpenId.Extensions.IExtensionResponse, new() {
+ throw new NotSupportedException(Strings.NotSupportedByAuthenticationSnapshot);
+ }
+
+ public DotNetOpenId.Extensions.IExtensionResponse GetExtension(Type extensionType) {
+ throw new NotSupportedException(Strings.NotSupportedByAuthenticationSnapshot);
+ }
+
+ public Identifier ClaimedIdentifier { get; private set; }
+
+ public string FriendlyIdentifierForDisplay { get; private set; }
+
+ public AuthenticationStatus Status { get; private set; }
+
+ public Exception Exception {
+ get { throw new NotSupportedException(Strings.NotSupportedByAuthenticationSnapshot); }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/CheckAuthRequest.cs b/src/DotNetOpenId/RelyingParty/CheckAuthRequest.cs
index 4fbf380..f46232a 100644
--- a/src/DotNetOpenId/RelyingParty/CheckAuthRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/CheckAuthRequest.cs
@@ -5,11 +5,12 @@ using System.Diagnostics;
namespace DotNetOpenId.RelyingParty {
class CheckAuthRequest : DirectRequest {
- CheckAuthRequest(ServiceEndpoint provider, IDictionary<string, string> args) :
- base(provider, args) {
+ CheckAuthRequest(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> args) :
+ base(relyingParty, provider, args) {
}
- public static CheckAuthRequest Create(ServiceEndpoint provider, IDictionary<string, string> query) {
+ public static CheckAuthRequest Create(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> query) {
+ if (relyingParty == null) throw new ArgumentNullException("relyingParty");
Protocol protocol = provider.Protocol;
string signed = query[protocol.openid.signed];
@@ -36,7 +37,7 @@ namespace DotNetOpenId.RelyingParty {
}
check_args[protocol.openid.mode] = protocol.Args.Mode.check_authentication;
- return new CheckAuthRequest(provider, check_args);
+ return new CheckAuthRequest(relyingParty, provider, check_args);
}
CheckAuthResponse response;
@@ -44,7 +45,7 @@ namespace DotNetOpenId.RelyingParty {
public CheckAuthResponse Response {
get {
if (response == null) {
- response = new CheckAuthResponse(Provider, GetResponse());
+ response = new CheckAuthResponse(RelyingParty, Provider, GetResponse());
}
return response;
}
diff --git a/src/DotNetOpenId/RelyingParty/CheckAuthResponse.cs b/src/DotNetOpenId/RelyingParty/CheckAuthResponse.cs
index 277a226..297fb82 100644
--- a/src/DotNetOpenId/RelyingParty/CheckAuthResponse.cs
+++ b/src/DotNetOpenId/RelyingParty/CheckAuthResponse.cs
@@ -6,8 +6,8 @@ using System.Diagnostics;
namespace DotNetOpenId.RelyingParty {
[DebuggerDisplay("IsAuthenticationValid: {IsAuthenticationValid}, OpenId: {Protocol.Version}")]
class CheckAuthResponse : DirectResponse {
- public CheckAuthResponse(ServiceEndpoint provider, IDictionary<string, string> args)
- : base(provider, args) {
+ public CheckAuthResponse(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> args)
+ : base(relyingParty, provider, args) {
}
public string InvalidatedAssociationHandle {
diff --git a/src/DotNetOpenId/RelyingParty/DirectMessageHttpChannel.cs b/src/DotNetOpenId/RelyingParty/DirectMessageHttpChannel.cs
new file mode 100644
index 0000000..781228d
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/DirectMessageHttpChannel.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net;
+
+namespace DotNetOpenId.RelyingParty {
+ internal class DirectMessageHttpChannel : IDirectMessageChannel {
+ #region IDirectMessageChannel Members
+
+ public IDictionary<string, string> SendDirectMessageAndGetResponse(ServiceEndpoint provider, IDictionary<string, string> fields) {
+ if (provider == null) throw new ArgumentNullException("provider");
+ if (fields == null) throw new ArgumentNullException("fields");
+
+ byte[] body = ProtocolMessages.Http.GetBytes(fields);
+ IDictionary<string, string> args;
+ UntrustedWebResponse resp = null;
+ string fullResponseText = null;
+ try {
+ resp = UntrustedWebRequest.Request(provider.ProviderEndpoint, body);
+ // If an internal server error occurred, there won't be any KV-form stream
+ // to read in. So instead, preserve whatever error the server did send back
+ // and throw it in the exception.
+ if (resp.StatusCode == HttpStatusCode.InternalServerError) {
+ string errorStream = new StreamReader(resp.ResponseStream).ReadToEnd();
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ProviderRespondedWithError, errorStream));
+ }
+ if (Logger.IsDebugEnabled) {
+ fullResponseText = resp.ReadResponseString();
+ }
+ args = ProtocolMessages.KeyValueForm.GetDictionary(resp.ResponseStream);
+ Logger.DebugFormat("Received direct response from {0}: {1}{2}", provider.ProviderEndpoint,
+ Environment.NewLine, Util.ToString(args));
+ } catch (ArgumentException e) {
+ Logger.DebugFormat("Full response from provider (where KVF was expected):{0}{1}",
+ Environment.NewLine, fullResponseText);
+ throw new OpenIdException("Failure decoding Key-Value Form response from provider.", e);
+ } catch (WebException e) {
+ throw new OpenIdException("Failure while connecting to provider.", e);
+ }
+ // All error codes are supposed to be returned with 400, but
+ // some (like myopenid.com) sometimes send errors as 200's.
+ if (resp.StatusCode == HttpStatusCode.BadRequest ||
+ Util.GetOptionalArg(args, provider.Protocol.openidnp.mode) == provider.Protocol.Args.Mode.error) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ProviderRespondedWithError,
+ Util.GetOptionalArg(args, provider.Protocol.openidnp.error)), args);
+ } else if (resp.StatusCode == HttpStatusCode.OK) {
+ return args;
+ } else {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ProviderRespondedWithUnrecognizedHTTPStatusCode, resp.StatusCode));
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/DirectRequest.cs b/src/DotNetOpenId/RelyingParty/DirectRequest.cs
index 9b9d732..2d0cc05 100644
--- a/src/DotNetOpenId/RelyingParty/DirectRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/DirectRequest.cs
@@ -9,9 +9,11 @@ using System.IO;
namespace DotNetOpenId.RelyingParty {
[DebuggerDisplay("OpenId: {Protocol.Version}")]
abstract class DirectRequest {
- protected DirectRequest(ServiceEndpoint provider, IDictionary<string, string> args) {
+ protected DirectRequest(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> args) {
+ if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (provider == null) throw new ArgumentNullException("provider");
if (args == null) throw new ArgumentNullException("args");
+ RelyingParty = relyingParty;
Provider = provider;
Args = args;
if (Protocol.QueryDeclaredNamespaceVersion != null &&
@@ -20,41 +22,13 @@ namespace DotNetOpenId.RelyingParty {
}
protected ServiceEndpoint Provider { get; private set; }
protected Protocol Protocol { get { return Provider.Protocol; } }
- protected IDictionary<string, string> Args { get; private set; }
+ protected internal IDictionary<string, string> Args { get; private set; }
+ protected OpenIdRelyingParty RelyingParty { get; private set; }
protected IDictionary<string, string> GetResponse() {
- byte[] body = ProtocolMessages.Http.GetBytes(Args);
- UntrustedWebResponse resp = null;
- IDictionary<string, string> args = null;
- try {
- resp = UntrustedWebRequest.Request(Provider.ProviderEndpoint, body);
- // If an internal server error occurred, there won't be any KV-form stream
- // to read in. So instead, preserve whatever error the server did send back
- // and throw it in the exception.
- if (resp.StatusCode == HttpStatusCode.InternalServerError) {
- string errorStream = new StreamReader(resp.ResponseStream).ReadToEnd();
- throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- Strings.ProviderRespondedWithError, errorStream));
- }
- args = ProtocolMessages.KeyValueForm.GetDictionary(resp.ResponseStream);
- } catch (ArgumentException e) {
- throw new OpenIdException("Failure decoding Key-Value Form response from provider.", e);
- } catch (WebException e) {
- throw new OpenIdException("Failure while connecting to provider.", e);
- }
- // All error codes are supposed to be returned with 400, but
- // some (like myopenid.com) sometimes send errors as 200's.
- if (resp.StatusCode == HttpStatusCode.BadRequest ||
- Util.GetOptionalArg(args, Protocol.openidnp.mode) == Protocol.Args.Mode.error) {
- throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- Strings.ProviderRespondedWithError,
- Util.GetOptionalArg(args, Protocol.openidnp.error)), args);
- } else if (resp.StatusCode == HttpStatusCode.OK) {
- return args;
- } else {
- throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
- Strings.ProviderRespondedWithUnrecognizedHTTPStatusCode, resp.StatusCode));
- }
+ Logger.DebugFormat("Sending direct message to {0}: {1}{2}", Provider.ProviderEndpoint,
+ Environment.NewLine, Util.ToString(Args));
+ return RelyingParty.DirectMessageChannel.SendDirectMessageAndGetResponse(Provider, Args);
}
}
}
diff --git a/src/DotNetOpenId/RelyingParty/DirectResponse.cs b/src/DotNetOpenId/RelyingParty/DirectResponse.cs
index 789ed0d..6a88a3a 100644
--- a/src/DotNetOpenId/RelyingParty/DirectResponse.cs
+++ b/src/DotNetOpenId/RelyingParty/DirectResponse.cs
@@ -2,28 +2,44 @@
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
+using System.Globalization;
namespace DotNetOpenId.RelyingParty {
[DebuggerDisplay("OpenId: {Protocol.Version}")]
class DirectResponse {
- protected DirectResponse(ServiceEndpoint provider, IDictionary<string, string> args) {
+ protected DirectResponse(OpenIdRelyingParty relyingParty, ServiceEndpoint provider, IDictionary<string, string> args) {
+ if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (provider == null) throw new ArgumentNullException("provider");
if (args == null) throw new ArgumentNullException("args");
+ RelyingParty = relyingParty;
Provider = provider;
Args = args;
- if (TraceUtil.Switch.TraceError) {
- if (!Args.ContainsKey(Protocol.openidnp.ns)) {
- Trace.TraceError("Direct response from provider lacked the {0} key.", Protocol.openid.ns);
- } else if (Args[Protocol.openidnp.ns] != Protocol.QueryDeclaredNamespaceVersion) {
- Trace.TraceError("Direct response from provider for key {0} was '{1}' rather than '{2}'.",
- Protocol.openid.ns, Args[Protocol.openidnp.ns], Protocol.QueryDeclaredNamespaceVersion);
- }
+ // Make sure that the OP fulfills the required OpenID version.
+ // We don't use Provider.Protocol here because that's just a cache of
+ // what we _thought_ the OP would support, and our purpose is to double-check this.
+ ProtocolVersion detectedProtocol = Protocol.DetectFromDirectResponse(args).ProtocolVersion;
+ if (detectedProtocol < relyingParty.Settings.MinimumRequiredOpenIdVersion) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.MinimumOPVersionRequirementNotMet,
+ Protocol.Lookup(relyingParty.Settings.MinimumRequiredOpenIdVersion).Version,
+ Protocol.Lookup(detectedProtocol).Version));
}
+ if (Logger.IsErrorEnabled) {
+ if (provider.Protocol.QueryDeclaredNamespaceVersion != null) {
+ if (!Args.ContainsKey(Protocol.openidnp.ns)) {
+ Logger.ErrorFormat("Direct response from provider lacked the {0} key.", Protocol.openid.ns);
+ } else if (Args[Protocol.openidnp.ns] != Protocol.QueryDeclaredNamespaceVersion) {
+ Logger.ErrorFormat("Direct response from provider for key {0} was '{1}' rather than '{2}'.",
+ Protocol.openid.ns, Args[Protocol.openidnp.ns], Protocol.QueryDeclaredNamespaceVersion);
+ }
+ }
+ }
}
+ protected OpenIdRelyingParty RelyingParty { get; private set; }
protected ServiceEndpoint Provider { get; private set; }
- protected IDictionary<string, string> Args { get; private set; }
+ protected internal IDictionary<string, string> Args { get; private set; }
protected Protocol Protocol { get { return Provider.Protocol; } }
}
}
diff --git a/src/DotNetOpenId/RelyingParty/FailedAuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/FailedAuthenticationResponse.cs
index 289760e..59abbc6 100644
--- a/src/DotNetOpenId/RelyingParty/FailedAuthenticationResponse.cs
+++ b/src/DotNetOpenId/RelyingParty/FailedAuthenticationResponse.cs
@@ -12,6 +12,14 @@ namespace DotNetOpenId.RelyingParty {
#region IAuthenticationResponse Members
+ public IDictionary<string, string> GetCallbackArguments() {
+ return new Dictionary<string, string>();
+ }
+
+ public string GetCallbackArgument(string key) {
+ return null;
+ }
+
public T GetExtension<T>() where T : DotNetOpenId.Extensions.IExtensionResponse, new() {
return default(T);
}
@@ -24,6 +32,10 @@ namespace DotNetOpenId.RelyingParty {
get { return null; }
}
+ public string FriendlyIdentifierForDisplay {
+ get { return null; }
+ }
+
public AuthenticationStatus Status {
get { return AuthenticationStatus.Failed; }
}
diff --git a/src/DotNetOpenId/RelyingParty/IAuthenticationRequest.cs b/src/DotNetOpenId/RelyingParty/IAuthenticationRequest.cs
index 0fdc4f1..a844da5 100644
--- a/src/DotNetOpenId/RelyingParty/IAuthenticationRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/IAuthenticationRequest.cs
@@ -11,14 +11,30 @@ namespace DotNetOpenId.RelyingParty {
/// </summary>
public interface IAuthenticationRequest {
/// <summary>
- /// Adds given key/value pairs to the query that the provider will use in
- /// the request to return to the consumer web site.
+ /// Makes a dictionary of key/value pairs available when the authentication is completed.
/// </summary>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against tampering in transit. No
+ /// security-sensitive data should be stored using this method.</para>
+ /// <para>The values stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArguments"/>.</para>
+ /// <para>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.</para>
+ /// </remarks>
void AddCallbackArguments(IDictionary<string, string> arguments);
/// <summary>
- /// Adds a given key/value pair to the query that the provider will use in
- /// the request to return to the consumer web site.
+ /// Makes a key/value pair available when the authentication is completed.
/// </summary>
+ /// <remarks>
+ /// <para>Note that these values are NOT protected against tampering in transit. No
+ /// security-sensitive data should be stored using this method.</para>
+ /// <para>The value stored here can be retrieved using
+ /// <see cref="IAuthenticationResponse.GetCallbackArgument"/>.</para>
+ /// <para>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.</para>
+ /// </remarks>
void AddCallbackArguments(string key, string value);
/// <summary>
/// Adds an OpenID extension to the request directed at the OpenID provider.
@@ -53,12 +69,34 @@ namespace DotNetOpenId.RelyingParty {
Realm Realm { get; }
/// <summary>
/// Gets the Claimed Identifier that the User Supplied Identifier
- /// resolved to.
+ /// resolved to. Null if the user provided an OP Identifier
+ /// (directed identity).
/// </summary>
+ /// <remarks>
+ /// 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
+ /// <see cref="IsDirectedIdentity"/> property rather than testing this
+ /// property for a null value.
+ /// </remarks>
Identifier ClaimedIdentifier { get; }
/// <summary>
+ /// Gets whether the authenticating user has chosen to let the Provider
+ /// determine and send the ClaimedIdentifier after authentication.
+ /// </summary>
+ bool IsDirectedIdentity { get; }
+ /// <summary>
+ /// Gets information about the OpenId Provider, as advertised by the
+ /// OpenId discovery documents found at the <see cref="ClaimedIdentifier"/>
+ /// location.
+ /// </summary>
+ IProviderEndpoint Provider { get; }
+ /// <summary>
/// The detected version of OpenID implemented by the Provider.
/// </summary>
+ [Obsolete("Use Provider.Version instead.")]
Version ProviderVersion { get; }
}
}
diff --git a/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs
index 382b080..c73859f 100644
--- a/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs
+++ b/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
+using System.Web;
using DotNetOpenId.Extensions;
namespace DotNetOpenId.RelyingParty {
@@ -17,6 +18,32 @@ namespace DotNetOpenId.RelyingParty {
/// </remarks>
public interface IAuthenticationResponse {
/// <summary>
+ /// Gets a callback argument's value that was previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.
+ /// </summary>
+ /// <returns>The value of the argument, or null if the named parameter could not be found.</returns>
+ /// <remarks>
+ /// <para>This may return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ string GetCallbackArgument(string key);
+ /// <summary>
+ /// Gets all the callback arguments that were previously added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/> or as a natural part
+ /// of the return_to URL.
+ /// </summary>
+ /// <returns>A name-value dictionary. Never null.</returns>
+ /// <remarks>
+ /// <para>This MAY return any argument on the querystring that came with the authentication response,
+ /// which may include parameters not explicitly added using
+ /// <see cref="IAuthenticationRequest.AddCallbackArguments(string, string)"/>.</para>
+ /// <para>Note that these values are NOT protected against tampering in transit.</para>
+ /// </remarks>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+ IDictionary<string, string> GetCallbackArguments();
+ /// <summary>
/// Tries to get an OpenID extension that may be present in the response.
/// </summary>
/// <returns>The extension, if it is found. Null otherwise.</returns>
@@ -28,10 +55,52 @@ namespace DotNetOpenId.RelyingParty {
/// <returns>The extension, if it is found. Null otherwise.</returns>
IExtensionResponse GetExtension(Type extensionType);
/// <summary>
- /// An Identifier that the end user claims to own.
+ /// An Identifier that the end user claims to own. For use with user database storage and lookup.
+ /// May be null for some failed authentications (i.e. failed directed identity authentications).
/// </summary>
+ /// <remarks>
+ /// <para>
+ /// This is the secure identifier that should be used for database storage and lookup.
+ /// It is not always friendly (i.e. =Arnott becomes =!9B72.7DD1.50A9.5CCD), but it protects
+ /// user identities against spoofing and other attacks.
+ /// </para>
+ /// <para>
+ /// For user-friendly identifiers to display, use the
+ /// <see cref="FriendlyIdentifierForDisplay"/> property.
+ /// </para>
+ /// </remarks>
Identifier ClaimedIdentifier { get; }
/// <summary>
+ /// Gets a user-friendly OpenID Identifier for display purposes ONLY.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This <i>should</i> be put through <see cref="HttpUtility.HtmlEncode(string)"/> before
+ /// sending to a browser to secure against javascript injection attacks.
+ /// </para>
+ /// <para>
+ /// This property retains some aspects of the user-supplied identifier that get lost
+ /// in the <see cref="ClaimedIdentifier"/>. For example, XRIs used as user-supplied
+ /// identifiers (i.e. =Arnott) become unfriendly unique strings (i.e. =!9B72.7DD1.50A9.5CCD).
+ /// For display purposes, such as text on a web page that says "You're logged in as ...",
+ /// this property serves to provide the =Arnott string, or whatever else is the most friendly
+ /// string close to what the user originally typed in.
+ /// </para>
+ /// <para>
+ /// If the user-supplied identifier is a URI, this property will be the URI after all
+ /// redirects, and with the protocol and fragment trimmed off.
+ /// If the user-supplied identifier is an XRI, this property will be the original XRI.
+ /// If the user-supplied identifier is an OpenID Provider identifier (i.e. yahoo.com),
+ /// this property will be the Claimed Identifier, with the protocol stripped if it is a URI.
+ /// </para>
+ /// <para>
+ /// It is <b>very</b> important that this property <i>never</i> be used for database storage
+ /// or lookup to avoid identity spoofing and other security risks. For database storage
+ /// and lookup please use the <see cref="ClaimedIdentifier"/> property.
+ /// </para>
+ /// </remarks>
+ string FriendlyIdentifierForDisplay { get; }
+ /// <summary>
/// The detailed success or failure status of the authentication attempt.
/// </summary>
AuthenticationStatus Status { get; }
diff --git a/src/DotNetOpenId/RelyingParty/IDirectMessageChannel.cs b/src/DotNetOpenId/RelyingParty/IDirectMessageChannel.cs
new file mode 100644
index 0000000..122f258
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/IDirectMessageChannel.cs
@@ -0,0 +1,7 @@
+using System.Collections.Generic;
+
+namespace DotNetOpenId.RelyingParty {
+ internal interface IDirectMessageChannel {
+ IDictionary<string, string> SendDirectMessageAndGetResponse(ServiceEndpoint provider, IDictionary<string, string> fields);
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/IProviderEndpoint.cs b/src/DotNetOpenId/RelyingParty/IProviderEndpoint.cs
new file mode 100644
index 0000000..6ba0704
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/IProviderEndpoint.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace DotNetOpenId.RelyingParty {
+ /// <summary>
+ /// Information published about an OpenId Provider by the
+ /// OpenId discovery documents found at a user's Claimed Identifier.
+ /// </summary>
+ /// <remarks>
+ /// Because information provided by this interface is suppplied by a
+ /// user's individually published documents, it may be incomplete or inaccurate.
+ /// </remarks>
+ public interface IProviderEndpoint {
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <typeparam name="T">The extension whose support is being queried.</typeparam>
+ /// <returns>True if support for the extension is advertised. False otherwise.</returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
+ bool IsExtensionSupported<T>() where T : Extensions.IExtension, new();
+ /// <summary>
+ /// Checks whether the OpenId Identifier claims support for a given extension.
+ /// </summary>
+ /// <param name="extensionType">The extension whose support is being queried.</param>
+ /// <returns>True if support for the extension is advertised. False otherwise.</returns>
+ /// <remarks>
+ /// Note that a true or false return value is no guarantee of a Provider's
+ /// support for or lack of support for an extension. The return value is
+ /// determined by how the authenticating user filled out his/her XRDS document only.
+ /// The only way to be sure of support for a given extension is to include
+ /// the extension in the request and see if a response comes back for that extension.
+ /// </remarks>
+ bool IsExtensionSupported(Type extensionType);
+ /// <summary>
+ /// The detected version of OpenID implemented by the Provider.
+ /// </summary>
+ Version Version { get; }
+ /// <summary>
+ /// The URL that the OpenID Provider receives authentication requests at.
+ /// </summary>
+ Uri Uri { get; }
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs
new file mode 100644
index 0000000..5c4f3dc
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/ISetupRequiredAuthenticationResponse.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.RelyingParty {
+ /// <summary>
+ /// An interface to expose useful properties and functionality for handling
+ /// authentication responses that are returned from Immediate authentication
+ /// requests that require a subsequent request to be made in non-immediate mode.
+ /// </summary>
+ public interface ISetupRequiredAuthenticationResponse {
+ /// <summary>
+ /// The <see cref="Identifier"/> to pass to <see cref="OpenIdRelyingParty.CreateRequest(Identifier)"/>
+ /// in a subsequent authentication attempt.
+ /// </summary>
+ /// <remarks>
+ /// When directed identity is used, this will be the Provider Identifier given by the user.
+ /// Otherwise it will be the Claimed Identifier derived from the user-supplied identifier.
+ /// </remarks>
+ Identifier ClaimedOrProviderIdentifier { get; }
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/IXrdsProviderEndpoint.cs b/src/DotNetOpenId/RelyingParty/IXrdsProviderEndpoint.cs
new file mode 100644
index 0000000..78c873f
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/IXrdsProviderEndpoint.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace DotNetOpenId.RelyingParty {
+ /// <summary>
+ /// An <see cref="IProviderEndpoint"/> interface with additional members for use
+ /// in sorting for most preferred endpoint.
+ /// </summary>
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xrds")]
+ public interface IXrdsProviderEndpoint : IProviderEndpoint {
+ /// <summary>
+ /// Checks for the presence of a given Type URI in an XRDS service.
+ /// </summary>
+ bool IsTypeUriPresent(string typeUri);
+ /// <summary>
+ /// Gets the priority associated with this service that may have been given
+ /// in the XRDS document.
+ /// </summary>
+ int? ServicePriority { get; }
+ /// <summary>
+ /// Gets the priority associated with the service endpoint URL.
+ /// </summary>
+ /// <remarks>
+ /// When sorting by priority, this property should be considered second after
+ /// <see cref="ServicePriority"/>.
+ /// </remarks>
+ int? UriPriority { get; }
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/IndirectMessageRequest.cs b/src/DotNetOpenId/RelyingParty/IndirectMessageRequest.cs
index 8348374..c562be1 100644
--- a/src/DotNetOpenId/RelyingParty/IndirectMessageRequest.cs
+++ b/src/DotNetOpenId/RelyingParty/IndirectMessageRequest.cs
@@ -16,9 +16,6 @@ namespace DotNetOpenId.RelyingParty {
public EncodingType EncodingType { get { return EncodingType.IndirectMessage ; } }
public IDictionary<string, string> EncodedFields { get; private set; }
public Uri RedirectUrl { get; private set; }
- public Protocol Protocol {
- get { throw new NotImplementedException(); }
- }
#endregion
}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs
new file mode 100644
index 0000000..4c7f145
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.cs
@@ -0,0 +1,773 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Web.UI;
+using System.Web.UI.WebControls;
+using DotNetOpenId.Extensions;
+
+[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedScriptResourceName, "text/javascript")]
+[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedDotNetOpenIdLogoResourceName, "image/gif")]
+[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedSpinnerResourceName, "image/gif")]
+[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginSuccessResourceName, "image/png")]
+[assembly: WebResource(DotNetOpenId.RelyingParty.OpenIdAjaxTextBox.EmbeddedLoginFailureResourceName, "image/png")]
+
+namespace DotNetOpenId.RelyingParty {
+ /// <summary>
+ /// An ASP.NET control that provides a minimal text box that is OpenID-aware and uses AJAX for
+ /// a premium login experience.
+ /// </summary>
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdAjaxTextBox runat=\"server\" />")]
+ public class OpenIdAjaxTextBox : WebControl {
+ internal const string EmbeddedScriptResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.OpenIdAjaxTextBox.js";
+ internal const string EmbeddedDotNetOpenIdLogoResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.dotnetopenid_16x16.gif";
+ internal const string EmbeddedSpinnerResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.spinner.gif";
+ internal const string EmbeddedLoginSuccessResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.login_success.png";
+ internal const string EmbeddedLoginFailureResourceName = DotNetOpenId.Util.DefaultNamespace + ".RelyingParty.login_failure.png";
+
+ #region Properties
+
+ const string authenticationResponseViewStateKey = "AuthenticationResponse";
+ const string authDataViewStateKey = "AuthData";
+ IAuthenticationResponse authenticationResponse;
+ /// <summary>
+ /// Gets the completed authentication response.
+ /// </summary>
+ public IAuthenticationResponse AuthenticationResponse {
+ get {
+ if (authenticationResponse == null) {
+ // We will either validate a new response and return a live AuthenticationResponse
+ // or we will try to deserialize a previous IAuthenticationResponse (snapshot)
+ // from viewstate and return that.
+ IAuthenticationResponse viewstateResponse = ViewState[authenticationResponseViewStateKey] as IAuthenticationResponse;
+ string viewstateAuthData = ViewState[authDataViewStateKey] as string;
+ string formAuthData = Page.Request.Form["openidAuthData"];
+
+ // First see if there is fresh auth data to be processed into a response.
+ if (formAuthData != null && !string.Equals(viewstateAuthData, formAuthData, StringComparison.Ordinal)) {
+ ViewState[authDataViewStateKey] = formAuthData;
+
+ Uri authUri = new Uri(formAuthData ?? viewstateAuthData);
+ var authDataFields = HttpUtility.ParseQueryString(authUri.Query);
+ var rp = new OpenIdRelyingParty(OpenIdRelyingParty.HttpApplicationStore,
+ authUri, authDataFields);
+ authenticationResponse = rp.Response;
+
+ // Save out the authentication response to viewstate so we can find it on
+ // a subsequent postback.
+ ViewState[authenticationResponseViewStateKey] = new AuthenticationResponseSnapshot(authenticationResponse);
+ } else {
+ authenticationResponse = viewstateResponse;
+ }
+ }
+ return authenticationResponse;
+ }
+ }
+
+ const string textViewStateKey = "Text";
+ /// <summary>
+ /// Gets/sets the value in the text field, completely unprocessed or normalized.
+ /// </summary>
+ [Bindable(true), DefaultValue(""), Category("Appearance")]
+ [Description("The value in the text field, completely unprocessed or normalized.")]
+ public string Text {
+ get { return (string)(ViewState[textViewStateKey] ?? string.Empty); }
+ set { ViewState[textViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string columnsViewStateKey = "Columns";
+ const int columnsDefault = 40;
+ /// <summary>
+ /// The width of the text box in characters.
+ /// </summary>
+ [Bindable(true), Category("Appearance"), DefaultValue(columnsDefault)]
+ [Description("The width of the text box in characters.")]
+ public int Columns {
+ get { return (int)(ViewState[columnsViewStateKey] ?? columnsDefault); }
+ set {
+ if (value < 0) throw new ArgumentOutOfRangeException("value");
+ ViewState[columnsViewStateKey] = value;
+ }
+ }
+
+ const string tabIndexViewStateKey = "TabIndex";
+ /// <summary>
+ /// Default value for <see cref="TabIndex"/> property.
+ /// </summary>
+ const short tabIndexDefault = 0;
+ /// <summary>
+ /// The tab index of the text box control. Use 0 to omit an explicit tabindex.
+ /// </summary>
+ [Bindable(true), Category("Behavior"), DefaultValue(tabIndexDefault)]
+ [Description("The tab index of the text box control. Use 0 to omit an explicit tabindex.")]
+ public override short TabIndex {
+ get { return (short)(ViewState[tabIndexViewStateKey] ?? tabIndexDefault); }
+ set { ViewState[tabIndexViewStateKey] = value; }
+ }
+
+ const string nameViewStateKey = "Name";
+ const string nameDefault = "openid_identifier";
+ /// <summary>
+ /// Gets/sets the HTML name to assign to the text field.
+ /// </summary>
+ [Bindable(true), DefaultValue(nameDefault), Category("Misc")]
+ [Description("The HTML name to assign to the text field.")]
+ public string Name {
+ get { return (string)(ViewState[nameViewStateKey] ?? nameDefault); }
+ set {
+ if (string.IsNullOrEmpty(value))
+ throw new ArgumentNullException("value");
+ ViewState[nameViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ const string timeoutViewStateKey = "Timeout";
+ readonly TimeSpan timeoutDefault = TimeSpan.FromSeconds(8);
+ /// <summary>
+ /// Gets/sets the time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.
+ /// </summary>
+ [Browsable(true), DefaultValue(typeof(TimeSpan), "00:00:08"), Category("Behavior")]
+ [Description("The time duration for the AJAX control to wait for an OP to respond before reporting failure to the user.")]
+ public TimeSpan Timeout {
+ get { return (TimeSpan)(ViewState[timeoutViewStateKey] ?? timeoutDefault); }
+ set {
+ if (value.TotalMilliseconds <= 0) throw new ArgumentOutOfRangeException("value");
+ ViewState[timeoutViewStateKey] = value;
+ }
+ }
+
+ const string logonTextViewStateKey = "LoginText";
+ const string logonTextDefault = "LOG IN";
+ /// <summary>
+ /// Gets/sets the text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(logonTextDefault), Localizable(true), Category("Appearance")]
+ [Description("The text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")]
+ public string LogOnText {
+ get { return (string)(ViewState[logonTextViewStateKey] ?? logonTextDefault); }
+ set {
+ if (string.IsNullOrEmpty(value))
+ throw new ArgumentNullException("value");
+ ViewState[logonTextViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ const string logonToolTipViewStateKey = "LoginToolTip";
+ const string logonToolTipDefault = "Click here to log in using a pop-up window.";
+ /// <summary>
+ /// Gets/sets the rool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(logonToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears on the LOG IN button in cases where immediate (invisible) authentication fails.")]
+ public string LogOnToolTip {
+ get { return (string)(ViewState[logonToolTipViewStateKey] ?? logonToolTipDefault); }
+ set { ViewState[logonToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string retryTextViewStateKey = "RetryText";
+ const string retryTextDefault = "RETRY";
+ /// <summary>
+ /// Gets/sets the text that appears on the RETRY button in cases where authentication times out.
+ /// </summary>
+ [Bindable(true), DefaultValue(retryTextDefault), Localizable(true), Category("Appearance")]
+ [Description("The text that appears on the RETRY button in cases where authentication times out.")]
+ public string RetryText {
+ get { return (string)(ViewState[retryTextViewStateKey] ?? retryTextDefault); }
+ set {
+ if (string.IsNullOrEmpty(value))
+ throw new ArgumentNullException("value");
+ ViewState[retryTextViewStateKey] = value ?? string.Empty;
+ }
+ }
+
+ const string retryToolTipViewStateKey = "RetryToolTip";
+ const string retryToolTipDefault = "Retry a failed identifier discovery.";
+ /// <summary>
+ /// Gets/sets the tool tip text that appears on the RETRY button in cases where authentication times out.
+ /// </summary>
+ [Bindable(true), DefaultValue(retryToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears on the RETRY button in cases where authentication times out.")]
+ public string RetryToolTip {
+ get { return (string)(ViewState[retryToolTipViewStateKey] ?? retryToolTipDefault); }
+ set { ViewState[retryToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string authenticationSucceededToolTipViewStateKey = "AuthenticationSucceededToolTip";
+ const string authenticationSucceededToolTipDefault = "Authenticated.";
+ /// <summary>
+ /// Gets/sets the tool tip text that appears when authentication succeeds.
+ /// </summary>
+ [Bindable(true), DefaultValue(authenticationSucceededToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears when authentication succeeds.")]
+ public string AuthenticationSucceededToolTip {
+ get { return (string)(ViewState[authenticationSucceededToolTipViewStateKey] ?? authenticationSucceededToolTipDefault); }
+ set { ViewState[authenticationSucceededToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string authenticationFailedToolTipViewStateKey = "AuthenticationFailedToolTip";
+ const string authenticationFailedToolTipDefault = "Authentication failed.";
+ /// <summary>
+ /// Gets/sets the tool tip text that appears when authentication fails.
+ /// </summary>
+ [Bindable(true), DefaultValue(authenticationFailedToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears when authentication fails.")]
+ public string AuthenticationFailedToolTip {
+ get { return (string)(ViewState[authenticationFailedToolTipViewStateKey] ?? authenticationFailedToolTipDefault); }
+ set { ViewState[authenticationFailedToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string busyToolTipViewStateKey = "BusyToolTip";
+ const string busyToolTipDefault = "Discovering/authenticating";
+ /// <summary>
+ /// Gets/sets the tool tip text that appears over the text box when it is discovering and authenticating.
+ /// </summary>
+ [Bindable(true), DefaultValue(busyToolTipDefault), Localizable(true), Category("Appearance")]
+ [Description("The tool tip text that appears over the text box when it is discovering and authenticating.")]
+ public string BusyToolTip {
+ get { return (string)(ViewState[busyToolTipViewStateKey] ?? busyToolTipDefault); }
+ set { ViewState[busyToolTipViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string identifierRequiredMessageViewStateKey = "BusyToolTip";
+ const string identifierRequiredMessageDefault = "Please correct errors in OpenID identifier and allow login to complete before submitting.";
+ /// <summary>
+ /// Gets/sets the message that is displayed if a postback is about to occur before the identifier has been supplied.
+ /// </summary>
+ [Bindable(true), DefaultValue(identifierRequiredMessageDefault), Localizable(true), Category("Appearance")]
+ [Description("The message that is displayed if a postback is about to occur before the identifier has been supplied.")]
+ public string IdentifierRequiredMessage {
+ get { return (string)(ViewState[identifierRequiredMessageViewStateKey] ?? identifierRequiredMessageDefault); }
+ set { ViewState[identifierRequiredMessageViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string logOnInProgressMessageViewStateKey = "BusyToolTip";
+ const string logOnInProgressMessageDefault = "Please wait for login to complete.";
+ /// <summary>
+ /// Gets/sets the message that is displayed if a postback is attempted while login is in process.
+ /// </summary>
+ [Bindable(true), DefaultValue(logOnInProgressMessageDefault), Localizable(true), Category("Appearance")]
+ [Description("The message that is displayed if a postback is attempted while login is in process.")]
+ public string LogOnInProgressMessage {
+ get { return (string)(ViewState[logOnInProgressMessageViewStateKey] ?? logOnInProgressMessageDefault); }
+ set { ViewState[logOnInProgressMessageViewStateKey] = value ?? string.Empty; }
+ }
+
+ const string realmUrlViewStateKey = "RealmUrl";
+ const string realmUrlDefault = "~/";
+ /// <summary>
+ /// The OpenID <see cref="Realm"/> of the relying party web site.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.Realm"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings"), SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")]
+ [Bindable(true)]
+ [Category("Behavior")]
+ [DefaultValue(realmUrlDefault)]
+ [Description("The OpenID Realm of the relying party web site.")]
+ public string RealmUrl {
+ get { return (string)(ViewState[realmUrlViewStateKey] ?? realmUrlDefault); }
+ set {
+ if (Page != null && !DesignMode) {
+ // Validate new value by trying to construct a Realm object based on it.
+ new Realm(Util.GetResolvedRealm(Page, value)); // throws an exception on failure.
+ } else {
+ // We can't fully test it, but it should start with either ~/ or a protocol.
+ if (Regex.IsMatch(value, @"^https?://")) {
+ new Uri(value.Replace("*.", "")); // make sure it's fully-qualified, but ignore wildcards
+ } else if (value.StartsWith("~/", StringComparison.Ordinal)) {
+ // this is valid too
+ } else
+ throw new UriFormatException();
+ }
+ ViewState[realmUrlViewStateKey] = value;
+ }
+ }
+
+ const string returnToUrlViewStateKey = "ReturnToUrl";
+ const string returnToUrlDefault = "";
+ /// <summary>
+ /// The OpenID ReturnTo of the relying party web site.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.ReturnTo"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings"), SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")]
+ [Bindable(true)]
+ [Category("Behavior")]
+ [DefaultValue(returnToUrlDefault)]
+ [Description("The OpenID ReturnTo of the relying party web site.")]
+ public string ReturnToUrl {
+ get { return (string)(ViewState[returnToUrlViewStateKey] ?? returnToUrlDefault); }
+ set {
+ if (Page != null && !DesignMode) {
+ // Validate new value by trying to construct a Uri based on it.
+ new Uri(Util.GetRequestUrlFromContext(), Page.ResolveUrl(value)); // throws an exception on failure.
+ } else {
+ // We can't fully test it, but it should start with either ~/ or a protocol.
+ if (Regex.IsMatch(value, @"^https?://")) {
+ new Uri(value); // make sure it's fully-qualified, but ignore wildcards
+ } else if (value.StartsWith("~/", StringComparison.Ordinal)) {
+ // this is valid too
+ } else {
+ throw new UriFormatException();
+ }
+ }
+ ViewState[returnToUrlViewStateKey] = value;
+ }
+ }
+
+ #endregion
+
+ #region Properties to hide
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override System.Drawing.Color ForeColor {
+ get { throw new NotSupportedException(); }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override System.Drawing.Color BackColor {
+ get { throw new NotSupportedException(); }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override System.Drawing.Color BorderColor {
+ get { throw new NotSupportedException(); }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override Unit BorderWidth {
+ get { return Unit.Empty; }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override BorderStyle BorderStyle {
+ get { return BorderStyle.None; }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override FontInfo Font {
+ get { return null; }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override Unit Height {
+ get { return Unit.Empty; }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override Unit Width {
+ get { return Unit.Empty; }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override string ToolTip {
+ get { return string.Empty; }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override string SkinID {
+ get { return string.Empty; }
+ set { throw new NotSupportedException(); }
+ }
+ /// <summary>
+ /// Unused property.
+ /// </summary>
+ [Browsable(false), Bindable(false)]
+ public override bool EnableTheming {
+ get { return false; }
+ set { throw new NotSupportedException(); }
+ }
+ #endregion
+
+ bool focusCalled;
+ /// <summary>
+ /// Places focus on the text box when the page is rendered on the browser.
+ /// </summary>
+ public override void Focus() {
+ focusCalled = true;
+ // we don't emit the code to focus the control immediately, in case the control
+ // is never rendered to the page because its Visible property is false or that
+ // of any of its parent containers.
+ }
+
+ #region Events
+
+ /// <summary>
+ /// Fired when the user has typed in their identifier, discovery was successful
+ /// and a login attempt is about to begin.
+ /// </summary>
+ [Description("Fired when the user has typed in their identifier, discovery was successful and a login attempt is about to begin.")]
+ public event EventHandler<OpenIdEventArgs> LoggingIn;
+ /// <summary>
+ /// Fires the <see cref="LoggingIn"/> event.
+ /// </summary>
+ protected virtual void OnLoggingIn(IAuthenticationRequest request) {
+ var loggingIn = LoggingIn;
+ if (loggingIn != null) {
+ loggingIn(this, new OpenIdEventArgs(request));
+ }
+ }
+
+ /// <summary>
+ /// Fired when a Provider sends back a positive assertion to this control,
+ /// but the authentication has not yet been verified.
+ /// </summary>
+ /// <remarks>
+ /// <b>No security critical decisions should be made within event handlers
+ /// for this event</b> as the authenticity of the assertion has not been
+ /// verified yet. All security related code should go in the event handler
+ /// for the <see cref="LoggedIn"/> event.
+ /// </remarks>
+ [Description("Fired when a Provider sends back a positive assertion to this control, but the authentication has not yet been verified.")]
+ public event EventHandler<OpenIdEventArgs> UnconfirmedPositiveAssertion;
+ /// <summary>
+ /// Fires the <see cref="UnconfirmedPositiveAssertion"/> event.
+ /// </summary>
+ protected virtual void OnUnconfirmedPositiveAssertion() {
+ var unconfirmedPositiveAssertion = UnconfirmedPositiveAssertion;
+ if (unconfirmedPositiveAssertion != null) {
+ unconfirmedPositiveAssertion(this, null);
+ }
+ }
+
+ /// <summary>
+ /// Fired when authentication has completed successfully.
+ /// </summary>
+ [Description("Fired when authentication has completed successfully.")]
+ public event EventHandler<OpenIdEventArgs> LoggedIn;
+ /// <summary>
+ /// Fires the <see cref="LoggedIn"/> event.
+ /// </summary>
+ protected virtual void OnLoggedIn(IAuthenticationResponse response) {
+ var loggedIn = LoggedIn;
+ if (loggedIn != null) {
+ loggedIn(this, new OpenIdEventArgs(response));
+ }
+ }
+
+ const string onClientAssertionReceivedViewStateKey = "OnClientAssertionReceived";
+ /// <summary>
+ /// Gets or sets the client-side script that executes when an authentication
+ /// assertion is received (but before it is verified).
+ /// </summary>
+ /// <remarks>
+ /// <para>In the context of the executing javascript set in this property, the
+ /// local variable <i>sender</i> is set to the openid_identifier input box
+ /// that is executing this code.
+ /// This variable has a getClaimedIdentifier() method that may be used to
+ /// identify the user who is being authenticated.</para>
+ /// <para>It is <b>very</b> important to note that when this code executes,
+ /// the authentication has not been verified and may have been spoofed.
+ /// No security-sensitive operations should take place in this javascript code.
+ /// The authentication is verified on the server by the time the
+ /// <see cref="LoggedIn"/> server-side event fires.</para>
+ /// </remarks>
+ [Description("Gets or sets the client-side script that executes when an authentication assertion is received (but before it is verified).")]
+ [Bindable(true), DefaultValue(""), Category("Behavior")]
+ public string OnClientAssertionReceived {
+ get { return ViewState[onClientAssertionReceivedViewStateKey] as string; }
+ set { ViewState[onClientAssertionReceivedViewStateKey] = value; }
+ }
+
+ #endregion
+
+ Dictionary<Type, string> clientScriptExtensions = new Dictionary<Type, string>();
+ /// <summary>
+ /// Allows an OpenID extension to read data out of an unverified positive authentication assertion
+ /// and send it down to the client browser so that Javascript running on the page can perform
+ /// some preprocessing on the extension data.
+ /// </summary>
+ /// <typeparam name="T">The extension <i>response</i> type that will read data from the assertion.</typeparam>
+ /// <param name="propertyName">The property name on the openid_identifier input box object that will be used to store the extension data. For example: sreg</param>
+ /// <remarks>
+ /// This method should be called from the <see cref="UnconfirmedPositiveAssertion"/> event handler.
+ /// </remarks>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
+ public void RegisterClientScriptExtension<T>(string propertyName) where T : IClientScriptExtensionResponse {
+ if (String.IsNullOrEmpty(propertyName)) throw new ArgumentNullException("propertyName");
+ if (clientScriptExtensions.ContainsValue(propertyName)) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ClientScriptExtensionPropertyNameCollision, propertyName), "propertyName");
+ }
+ foreach (var ext in clientScriptExtensions.Keys) {
+ if (ext == typeof(T)) {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ClientScriptExtensionTypeCollision, typeof(T).FullName));
+ }
+ }
+ clientScriptExtensions.Add(typeof(T), propertyName);
+ }
+
+ /// <summary>
+ /// Prepares the control for loading.
+ /// </summary>
+ protected override void OnLoad(EventArgs e) {
+ base.OnLoad(e);
+
+ if (Page.IsPostBack) {
+ // If the control was temporarily hidden, it won't be in the Form data,
+ // and we'll just implicitly keep the last Text setting.
+ if (Page.Request.Form[Name] != null) {
+ Text = Page.Request.Form[Name];
+ }
+
+ // If there is a response, and it is fresh (live object, not a snapshot object)...
+ if (AuthenticationResponse != null && AuthenticationResponse is AuthenticationResponse) {
+ switch (AuthenticationResponse.Status) {
+ case AuthenticationStatus.Authenticated:
+ OnLoggedIn(AuthenticationResponse);
+ break;
+ default:
+ break;
+ }
+ }
+ } else {
+ NameValueCollection query = Util.GetQueryOrFormFromContextNVC();
+ string userSuppliedIdentifier = query["dotnetopenid.userSuppliedIdentifier"];
+ if (!string.IsNullOrEmpty(userSuppliedIdentifier)) {
+ Logger.Info("AJAX (iframe) request detected.");
+ if (query["dotnetopenid.phase"] == "2") {
+ OnUnconfirmedPositiveAssertion();
+ reportDiscoveryResult();
+ } else {
+ performDiscovery(userSuppliedIdentifier);
+ }
+ }
+ }
+ }
+
+ private void prepareClientJavascript() {
+ // Import the .js file where most of the code is.
+ Page.ClientScript.RegisterClientScriptResource(typeof(OpenIdAjaxTextBox), EmbeddedScriptResourceName);
+ // Call into the .js file with initialization information.
+ StringBuilder startupScript = new StringBuilder();
+ startupScript.AppendLine("<script language='javascript'>");
+ startupScript.AppendFormat("var box = document.getElementsByName('{0}')[0];{1}", Name, Environment.NewLine);
+ if (focusCalled) {
+ startupScript.AppendLine("box.focus();");
+ }
+ startupScript.AppendFormat(CultureInfo.InvariantCulture,
+ "initAjaxOpenId(box, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15});{16}",
+ Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), OpenIdTextBox.EmbeddedLogoResourceName)),
+ Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedDotNetOpenIdLogoResourceName)),
+ Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedSpinnerResourceName)),
+ Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedLoginSuccessResourceName)),
+ Util.GetSafeJavascriptValue(Page.ClientScript.GetWebResourceUrl(GetType(), EmbeddedLoginFailureResourceName)),
+ Timeout.TotalMilliseconds,
+ string.IsNullOrEmpty(OnClientAssertionReceived) ? "null" : "'" + OnClientAssertionReceived.Replace(@"\", @"\\").Replace("'", @"\'") + "'",
+ Util.GetSafeJavascriptValue(LogOnText),
+ Util.GetSafeJavascriptValue(LogOnToolTip),
+ Util.GetSafeJavascriptValue(RetryText),
+ Util.GetSafeJavascriptValue(RetryToolTip),
+ Util.GetSafeJavascriptValue(BusyToolTip),
+ Util.GetSafeJavascriptValue(IdentifierRequiredMessage),
+ Util.GetSafeJavascriptValue(LogOnInProgressMessage),
+ Util.GetSafeJavascriptValue(AuthenticationSucceededToolTip),
+ Util.GetSafeJavascriptValue(AuthenticationFailedToolTip),
+ Environment.NewLine);
+
+ if (AuthenticationResponse != null && AuthenticationResponse.Status == AuthenticationStatus.Authenticated) {
+ startupScript.AppendFormat("box.dnoi_internal.openidAuthResult('{0}');{1}", ViewState[authDataViewStateKey].ToString().Replace("'", "\\'"), Environment.NewLine);
+ }
+ startupScript.AppendLine("</script>");
+
+ Page.ClientScript.RegisterStartupScript(GetType(), "ajaxstartup", startupScript.ToString());
+ Page.ClientScript.RegisterOnSubmitStatement(GetType(), "loginvalidation", string.Format(CultureInfo.InvariantCulture, @"
+var openidbox = document.getElementsByName('{0}')[0];
+if (!openidbox.dnoi_internal.onSubmit()) {{ return false; }}
+", Name));
+ }
+
+ private IAuthenticationRequest createRequest(Identifier userSuppliedIdentifier) {
+ IAuthenticationRequest request;
+
+ OpenIdRelyingParty rp = new OpenIdRelyingParty();
+
+ // Approximate the returnTo (either based on the customize property or the page URL)
+ // so we can use it to help with Realm resolution.
+ Uri returnToApproximation = ReturnToUrl != null ? new Uri(Util.GetRequestUrlFromContext(), ReturnToUrl) : Page.Request.Url;
+
+ // Resolve the trust root, and swap out the scheme and port if necessary to match the
+ // return_to URL, since this match is required by OpenId, and the consumer app
+ // may be using HTTP at some times and HTTPS at others.
+ UriBuilder realm = Util.GetResolvedRealm(Page, RealmUrl);
+ realm.Scheme = returnToApproximation.Scheme;
+ realm.Port = returnToApproximation.Port;
+
+ // Initiate openid request
+ // We use TryParse here to avoid throwing an exception which
+ // might slip through our validator control if it is disabled.
+ Realm typedRealm = new Realm(realm);
+ if (string.IsNullOrEmpty(ReturnToUrl)) {
+ request = rp.CreateRequest(userSuppliedIdentifier, typedRealm);
+ } else {
+ // Since the user actually gave us a return_to value,
+ // the "approximation" is exactly what we want.
+ request = rp.CreateRequest(userSuppliedIdentifier, typedRealm, returnToApproximation);
+ }
+
+ return request;
+ }
+
+ private void performDiscovery(string userSuppliedIdentifier) {
+ if (String.IsNullOrEmpty(userSuppliedIdentifier)) throw new ArgumentNullException("userSuppliedIdentifier");
+ NameValueCollection query = Util.GetQueryOrFormFromContextNVC();
+ Logger.InfoFormat("Discovery on {0} requested.", userSuppliedIdentifier);
+
+ try {
+ IAuthenticationRequest req = createRequest(userSuppliedIdentifier);
+ // If the ReturnToUrl was explicitly set, we'll need to reset our first parameter
+ if (string.IsNullOrEmpty(HttpUtility.ParseQueryString(req.ReturnToUrl.Query)["dotnetopenid.userSuppliedIdentifier"])) {
+ req.AddCallbackArguments("dotnetopenid.userSuppliedIdentifier", userSuppliedIdentifier);
+ }
+ req.AddCallbackArguments("dotnetopenid.phase", "2");
+ if (query["dotnetopenid.immediate"] == "true") {
+ req.Mode = AuthenticationRequestMode.Immediate;
+ }
+ OnLoggingIn(req);
+ req.RedirectToProvider();
+ } catch (OpenIdException ex) {
+ callbackUserAgentMethod("dnoi_internal.openidDiscoveryFailure('" + ex.Message.Replace("'", "\\'") + "')");
+ }
+ }
+
+ private void reportDiscoveryResult() {
+ Logger.InfoFormat("AJAX (iframe) callback from OP: {0}", Page.Request.Url);
+ List<string> assignments = new List<string>();
+
+ OpenIdRelyingParty rp = new OpenIdRelyingParty();
+ var f = Util.NameValueCollectionToDictionary(HttpUtility.ParseQueryString(Page.Request.Url.Query));
+ var authResponse = RelyingParty.AuthenticationResponse.Parse(f, rp, Page.Request.Url, false);
+ if (authResponse.Status == AuthenticationStatus.Authenticated) {
+ foreach (var pair in clientScriptExtensions) {
+ string js = authResponse.GetExtensionClientScript(pair.Key);
+ if (string.IsNullOrEmpty(js)) {
+ js = "null";
+ }
+ assignments.Add(pair.Value + " = " + js);
+ }
+ }
+
+ callbackUserAgentMethod("dnoi_internal.openidAuthResult(document.URL)", assignments.ToArray());
+ }
+
+ /// <summary>
+ /// Prepares to render the control.
+ /// </summary>
+ protected override void OnPreRender(EventArgs e) {
+ base.OnPreRender(e);
+
+ prepareClientJavascript();
+ }
+
+ /// <summary>
+ /// Renders the control.
+ /// </summary>
+ protected override void Render(System.Web.UI.HtmlTextWriter writer) {
+ // We surround the textbox with a span so that the .js file can inject a
+ // login button within the text box with easy placement.
+ writer.WriteBeginTag("span");
+ writer.WriteAttribute("class", CssClass);
+ writer.Write(" style='");
+ writer.WriteStyleAttribute("position", "relative");
+ writer.WriteStyleAttribute("font-size", "16px");
+ writer.Write("'>");
+
+ writer.WriteBeginTag("input");
+ writer.WriteAttribute("name", Name);
+ writer.WriteAttribute("id", ClientID);
+ writer.WriteAttribute("value", Text);
+ writer.WriteAttribute("size", Columns.ToString(CultureInfo.InvariantCulture));
+ if (TabIndex > 0) {
+ writer.WriteAttribute("tabindex", TabIndex.ToString(CultureInfo.InvariantCulture));
+ }
+ if (!Enabled) {
+ writer.WriteAttribute("disabled", "true");
+ }
+ if (!string.IsNullOrEmpty(CssClass)) {
+ writer.WriteAttribute("class", CssClass);
+ }
+ writer.Write(" style='");
+ writer.WriteStyleAttribute("padding-left", "18px");
+ writer.WriteStyleAttribute("border-style", "solid");
+ writer.WriteStyleAttribute("border-width", "1px");
+ writer.WriteStyleAttribute("border-color", "lightgray");
+ writer.Write("'");
+ writer.Write(" />");
+
+ writer.WriteEndTag("span");
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ private void callbackUserAgentMethod(string methodCall) {
+ callbackUserAgentMethod(methodCall, null);
+ }
+
+ /// <summary>
+ /// Invokes a method on a parent frame/window's OpenIdAjaxTextBox,
+ /// and closes the calling popup window if applicable.
+ /// </summary>
+ /// <param name="methodCall">The method to call on the OpenIdAjaxTextBox, including
+ /// parameters. (i.e. "callback('arg1', 2)"). No escaping is done by this method.</param>
+ /// <param name="preAssignments">An optional list of assignments to make to the input box object before placing the method call.</param>
+ private void callbackUserAgentMethod(string methodCall, string[] preAssignments) {
+ Logger.InfoFormat("Sending Javascript callback: {0}", methodCall);
+ Page.Response.Write(@"<html><body><script language='javascript'>
+ var inPopup = !window.frameElement;
+ var objSrc = inPopup ? window.opener.waiting_openidBox : window.frameElement.openidBox;
+");
+ if (preAssignments != null) {
+ foreach (string assignment in preAssignments) {
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture, " objSrc.{0};\n", assignment));
+ }
+ }
+ // Something about calling objSrc.{0} can somehow cause FireFox to forget about the inPopup variable,
+ // so we have to actually put the test for it ABOVE the call to objSrc.{0} so that it already
+ // whether to call window.self.close() after the call.
+ Page.Response.Write(string.Format(CultureInfo.InvariantCulture,
+@" if (inPopup) {{
+ objSrc.{0};
+ window.self.close();
+}} else {{
+ objSrc.{0};
+}}
+</script></body></html>", methodCall));
+ Page.Response.End();
+ }
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js
new file mode 100644
index 0000000..e2ef0a0
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/OpenIdAjaxTextBox.js
@@ -0,0 +1,455 @@
+// Options that can be set on the host page:
+// window.openid_visible_iframe = true; // causes the hidden iframe to show up
+// window.openid_trace = true; // causes lots of alert boxes
+
+function trace(msg) {
+ if (window.openid_trace) {
+ alert(msg);
+ }
+}
+
+function initAjaxOpenId(box, openid_logo_url, dotnetopenid_logo_url, spinner_url, success_icon_url, failure_icon_url,
+ timeout, assertionReceivedCode,
+ loginButtonText, loginButtonToolTip, retryButtonText, retryButtonToolTip, busyToolTip,
+ identifierRequiredMessage, loginInProgressMessage,
+ authenticationSucceededToolTip, authenticationFailedToolTip) {
+ box.dnoi_internal = new Object();
+ if (assertionReceivedCode) {
+ box.dnoi_internal.onauthenticated = function(sender, e) { eval(assertionReceivedCode); }
+ }
+
+ box.dnoi_internal.originalBackground = box.style.background;
+ box.timeout = timeout;
+
+ box.dnoi_internal.constructButton = function(text, tooltip, onclick) {
+ var button = document.createElement('button');
+ button.textContent = text; // Mozilla
+ button.value = text; // IE
+ button.title = tooltip;
+ button.onclick = onclick;
+ button.style.visibility = 'hidden';
+ button.style.position = 'absolute';
+ button.style.padding = "0px";
+ button.style.fontSize = '8px';
+ button.style.top = "1px";
+ button.style.bottom = "1px";
+ button.style.right = "2px";
+ box.parentNode.appendChild(button);
+ return button;
+ }
+
+ box.dnoi_internal.constructIcon = function(imageUrl, tooltip, rightSide, visible, height) {
+ var icon = document.createElement('img');
+ icon.src = imageUrl;
+ icon.title = tooltip != null ? tooltip : '';
+ if (!visible) {
+ icon.style.visibility = 'hidden';
+ }
+ icon.style.position = 'absolute';
+ icon.style.top = "2px";
+ icon.style.bottom = "2px"; // for FireFox (and IE7, I think)
+ if (height) {
+ icon.style.height = height; // for Chrome and IE8
+ }
+ if (rightSide) {
+ icon.style.right = "2px";
+ } else {
+ icon.style.left = "2px";
+ }
+ box.parentNode.appendChild(icon);
+ return icon;
+ }
+
+ box.dnoi_internal.prefetchImage = function(imageUrl) {
+ var img = document.createElement('img');
+ img.src = imageUrl;
+ img.style.display = 'none';
+ box.parentNode.appendChild(img);
+ return img;
+ }
+
+ box.dnoi_internal.loginButton = box.dnoi_internal.constructButton(loginButtonText, loginButtonToolTip, function() {
+ box.dnoi_internal.popup = window.open(box.dnoi_internal.getAuthenticationUrl(), 'opLogin', 'status=0,toolbar=0,location=1,resizable=1,scrollbars=1,width=800,height=600');
+ self.waiting_openidBox = box;
+ return false;
+ });
+ box.dnoi_internal.retryButton = box.dnoi_internal.constructButton(retryButtonText, retryButtonToolTip, function() {
+ box.timeout += 5000; // give the retry attempt 5s longer than the last attempt
+ box.dnoi_internal.performDiscovery();
+ return false;
+ });
+ box.dnoi_internal.openid_logo = box.dnoi_internal.constructIcon(openid_logo_url, null, false, true);
+ box.dnoi_internal.op_logo = box.dnoi_internal.constructIcon('', null, false, false, "16px");
+ box.dnoi_internal.spinner = box.dnoi_internal.constructIcon(spinner_url, busyToolTip, true);
+ box.dnoi_internal.success_icon = box.dnoi_internal.constructIcon(success_icon_url, authenticationSucceededToolTip, true);
+ //box.dnoi_internal.failure_icon = box.dnoi_internal.constructIcon(failure_icon_url, authenticationFailedToolTip, true);
+
+ // Disable the display of the DotNetOpenId logo
+ //box.dnoi_internal.dnoi_logo = box.dnoi_internal.constructIcon(dotnetopenid_logo_url);
+ box.dnoi_internal.dnoi_logo = box.dnoi_internal.openid_logo;
+
+ box.dnoi_internal.setVisualCue = function(state) {
+ box.dnoi_internal.openid_logo.style.visibility = 'hidden';
+ box.dnoi_internal.dnoi_logo.style.visibility = 'hidden';
+ box.dnoi_internal.op_logo.style.visibility = 'hidden';
+ box.dnoi_internal.spinner.style.visibility = 'hidden';
+ box.dnoi_internal.success_icon.style.visibility = 'hidden';
+// box.dnoi_internal.failure_icon.style.visibility = 'hidden';
+ box.dnoi_internal.loginButton.style.visibility = 'hidden';
+ box.dnoi_internal.retryButton.style.visibility = 'hidden';
+ box.title = null;
+ if (state == "discovering") {
+ box.dnoi_internal.dnoi_logo.style.visibility = 'visible';
+ box.dnoi_internal.spinner.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ box.title = null;
+ window.status = "Discovering OpenID Identifier '" + box.value + "'...";
+ } else if (state == "authenticated") {
+ var opLogo = box.dnoi_internal.deriveOPFavIcon();
+ if (opLogo) {
+ box.dnoi_internal.op_logo.src = opLogo;
+ box.dnoi_internal.op_logo.style.visibility = 'visible';
+ } else {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ }
+ box.dnoi_internal.success_icon.style.visibility = 'visible';
+ box.title = box.dnoi_internal.claimedIdentifier;
+ window.status = "Authenticated as " + box.value;
+ } else if (state == "setup") {
+ var opLogo = box.dnoi_internal.deriveOPFavIcon();
+ if (opLogo) {
+ box.dnoi_internal.op_logo.src = opLogo;
+ box.dnoi_internal.op_logo.style.visibility = 'visible';
+ } else {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ }
+ box.dnoi_internal.loginButton.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = "Authentication requires setup.";
+ } else if (state == "failed") {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ //box.dnoi_internal.failure_icon.style.visibility = 'visible';
+ box.dnoi_internal.retryButton.style.visibility = 'visible';
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = authenticationFailedToolTip;
+ box.title = authenticationFailedToolTip;
+ } else if (state = '' || state == null) {
+ box.dnoi_internal.openid_logo.style.visibility = 'visible';
+ box.title = null;
+ box.dnoi_internal.claimedIdentifier = null;
+ window.status = null;
+ } else {
+ box.dnoi_internal.claimedIdentifier = null;
+ trace('unrecognized state ' + state);
+ }
+ }
+
+ box.dnoi_internal.isBusy = function() {
+ return box.discoveryIFrame != null;
+ };
+
+ box.dnoi_internal.onSubmit = function() {
+ if (box.lastAuthenticationResult != 'authenticated') {
+ if (box.dnoi_internal.isBusy()) {
+ alert(loginInProgressMessage);
+ } else {
+ if (box.value.length > 0) {
+ // submitPending will be true if we've already tried deferring submit for a login,
+ // in which case we just want to display a box to the user.
+ if (box.dnoi_internal.submitPending) {
+ alert(identifierRequiredMessage);
+ } else {
+ // The user hasn't clicked "Login" yet. We'll click login for him,
+ // after leaving a note for ourselves to automatically click submit
+ // when login is complete.
+ box.dnoi_internal.submitPending = box.dnoi_internal.submitButtonJustClicked;
+ if (box.dnoi_internal.submitPending == null) {
+ box.dnoi_internal.submitPending = true;
+ }
+ box.dnoi_internal.loginButton.onclick();
+ return false; // abort submit for now
+ }
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+ return true;
+ };
+
+ box.dnoi_internal.setLastSubmitButtonClicked = function(evt) {
+ var button;
+ if (evt.target) {
+ button = evt.target;
+ } else {
+ button = evt.srcElement;
+ }
+
+ box.dnoi_internal.submitButtonJustClicked = button;
+ };
+
+ // box.hookAllSubmitElements = function(searchNode) {
+ var inputs = document.getElementsByTagName('input');
+ for (var i = 0; i < inputs.length; i++) {
+ var el = inputs[i];
+ if (el.type == 'submit') {
+ if (el.attachEvent) {
+ el.attachEvent("onclick", box.dnoi_internal.setLastSubmitButtonClicked);
+ } else {
+ el.addEventListener("click", box.dnoi_internal.setLastSubmitButtonClicked, true);
+ }
+ }
+ }
+ //};
+
+ box.dnoi_internal.getAuthenticationUrl = function(immediateMode) {
+ var frameLocation = new Uri(document.location.href);
+ var discoveryUri = frameLocation.trimFragment();
+ discoveryUri.appendQueryVariable('dotnetopenid.userSuppliedIdentifier', box.value);
+ if (immediateMode) {
+ discoveryUri.appendQueryVariable('dotnetopenid.immediate', 'true');
+ }
+ return discoveryUri;
+ };
+
+ box.dnoi_internal.performDiscovery = function() {
+ box.dnoi_internal.closeDiscoveryIFrame();
+ box.dnoi_internal.setVisualCue('discovering');
+ box.lastDiscoveredIdentifier = box.value;
+ box.lastAuthenticationResult = null;
+ var discoveryUri = box.dnoi_internal.getAuthenticationUrl(true);
+ if (box.discoveryIFrame) {
+ box.discoveryIFrame.parentNode.removeChild(box.discoveryIFrame);
+ box.discoveryIFrame = null;
+ }
+ trace('Performing discovery using url: ' + discoveryUri);
+ box.discoveryIFrame = createHiddenFrame(discoveryUri);
+ };
+
+ function findParentForm(element) {
+ if (element == null || element.nodeName == "FORM") {
+ return element;
+ }
+
+ return findParentForm(element.parentNode);
+ };
+
+ function findOrCreateHiddenField(form, name) {
+ if (box.hiddenField) {
+ return box.hiddenField;
+ }
+
+ box.hiddenField = document.createElement('input');
+ box.hiddenField.setAttribute("name", name);
+ box.hiddenField.setAttribute("type", "hidden");
+ form.appendChild(box.hiddenField);
+ return box.hiddenField;
+ };
+
+ box.dnoi_internal.deriveOPFavIcon = function() {
+ if (!box.hiddenField) return;
+ var authResult = new Uri(box.hiddenField.value);
+ var opUri;
+ if (authResult.getQueryArgValue("openid.op_endpoint")) {
+ opUri = new Uri(authResult.getQueryArgValue("openid.op_endpoint"));
+ } else if (authResult.getQueryArgValue("openid.user_setup_url")) {
+ opUri = new Uri(authResult.getQueryArgValue("openid.user_setup_url"));
+ } else return null;
+ var favicon = opUri.getAuthority() + "/favicon.ico";
+ return favicon;
+ };
+
+ function createHiddenFrame(url) {
+ var iframe = document.createElement("iframe");
+ if (!window.openid_visible_iframe) {
+ iframe.setAttribute("width", 0);
+ iframe.setAttribute("height", 0);
+ iframe.setAttribute("style", "display: none");
+ }
+ iframe.setAttribute("src", url);
+ iframe.openidBox = box;
+ box.parentNode.insertBefore(iframe, box);
+ box.discoveryTimeout = setTimeout(function() { trace("timeout"); box.dnoi_internal.openidDiscoveryFailure("Timed out"); }, box.timeout);
+ return iframe;
+ };
+
+ box.parentForm = findParentForm(box);
+
+ box.dnoi_internal.openidDiscoveryFailure = function(msg) {
+ box.dnoi_internal.closeDiscoveryIFrame();
+ trace('Discovery failure: ' + msg);
+ box.lastAuthenticationResult = 'failed';
+ box.dnoi_internal.setVisualCue('failed');
+ box.title = msg;
+ };
+
+ box.dnoi_internal.closeDiscoveryIFrame = function() {
+ if (box.discoveryTimeout) {
+ clearTimeout(box.discoveryTimeout);
+ }
+ if (box.discoveryIFrame) {
+ box.discoveryIFrame.parentNode.removeChild(box.discoveryIFrame);
+ box.discoveryIFrame = null;
+ }
+ };
+
+ box.dnoi_internal.openidAuthResult = function(resultUrl) {
+ self.waiting_openidBox = null;
+ trace('openidAuthResult ' + resultUrl);
+ if (box.discoveryIFrame) {
+ box.dnoi_internal.closeDiscoveryIFrame();
+ } else if (box.dnoi_internal.popup) {
+ box.dnoi_internal.popup.close();
+ box.dnoi_internal.popup = null;
+ }
+ var resultUri = new Uri(resultUrl);
+
+ // stick the result in a hidden field so the RP can verify it (positive or negative)
+ var form = findParentForm(box);
+ var hiddenField = findOrCreateHiddenField(form, "openidAuthData");
+ hiddenField.setAttribute("value", resultUri.toString());
+ trace("set openidAuthData = " + resultUri.queryString);
+ if (hiddenField.parentNode == null) {
+ form.appendChild(hiddenField);
+ }
+ trace("review: " + box.hiddenField.value);
+
+ if (isAuthSuccessful(resultUri)) {
+ // visual cue that auth was successful
+ box.dnoi_internal.claimedIdentifier = isOpenID2Response(resultUri) ? resultUri.getQueryArgValue("openid.claimed_id") : resultUri.getQueryArgValue("openid.identity");
+ box.dnoi_internal.setVisualCue('authenticated');
+ box.lastAuthenticationResult = 'authenticated';
+ if (box.dnoi_internal.onauthenticated) {
+ box.dnoi_internal.onauthenticated(box);
+ }
+ if (box.dnoi_internal.submitPending) {
+ // We submit the form BEFORE resetting the submitPending so
+ // the submit handler knows we've already tried this route.
+ if (box.dnoi_internal.submitPending == true) {
+ box.parentForm.submit();
+ } else {
+ box.dnoi_internal.submitPending.click();
+ }
+ }
+ } else {
+ // visual cue that auth failed
+ box.dnoi_internal.setVisualCue('setup');
+ box.lastAuthenticationResult = 'setup';
+ }
+
+ box.dnoi_internal.submitPending = null;
+ };
+
+ function isAuthSuccessful(resultUri) {
+ if (isOpenID2Response(resultUri)) {
+ return resultUri.getQueryArgValue("openid.mode") == "id_res";
+ } else {
+ return resultUri.getQueryArgValue("openid.mode") == "id_res" && !resultUri.containsQueryArg("openid.user_setup_url");
+ }
+ };
+
+ function isOpenID2Response(resultUri) {
+ return resultUri.containsQueryArg("openid.ns");
+ };
+
+ box.onblur = function(event) {
+ if (box.lastDiscoveredIdentifier != box.value) {
+ if (box.value.length > 0) {
+ box.dnoi_internal.performDiscovery();
+ } else {
+ box.dnoi_internal.setVisualCue();
+ }
+ box.oldvalue = box.value;
+ }
+ return true;
+ };
+ box.onkeyup = function(event) {
+ if (box.lastDiscoveredIdentifier != box.value) {
+ box.dnoi_internal.setVisualCue();
+ } else {
+ box.dnoi_internal.setVisualCue(box.lastAuthenticationResult);
+ }
+ return true;
+ };
+ box.getClaimedIdentifier = function() { return box.dnoi_internal.claimedIdentifier; };
+}
+
+function Uri(url) {
+ this.originalUri = url;
+
+ this.toString = function() {
+ return this.originalUri;
+ };
+
+ this.getAuthority = function() {
+ var authority = this.getScheme() + "://" + this.getHost();
+ return authority;
+ }
+
+ this.getHost = function() {
+ var hostStartIdx = this.originalUri.indexOf("://") + 3;
+ var hostEndIndex = this.originalUri.indexOf("/", hostStartIdx);
+ if (hostEndIndex < 0) hostEndIndex = this.originalUri.length;
+ var host = this.originalUri.substr(hostStartIdx, hostEndIndex - hostStartIdx);
+ return host;
+ }
+
+ this.getScheme = function() {
+ var schemeStartIdx = this.indexOf("://");
+ return this.originalUri.substr(this.originalUri, schemeStartIdx);
+ }
+
+ this.trimFragment = function() {
+ var hashmark = this.originalUri.indexOf('#');
+ if (hashmark >= 0) {
+ return new Uri(this.originalUri.substr(0, hashmark));
+ }
+ return this;
+ };
+
+ this.appendQueryVariable = function(name, value) {
+ var pair = encodeURI(name) + "=" + encodeURI(value);
+ if (this.originalUri.indexOf('?') >= 0) {
+ this.originalUri = this.originalUri + "&" + pair;
+ } else {
+ this.originalUri = this.originalUri + "?" + pair;
+ }
+ };
+
+ function KeyValuePair(key, value) {
+ this.key = key;
+ this.value = value;
+ };
+
+ this.Pairs = Array();
+
+ var queryBeginsAt = this.originalUri.indexOf('?');
+ if (queryBeginsAt >= 0) {
+ this.queryString = url.substr(queryBeginsAt + 1);
+ var queryStringPairs = this.queryString.split('&');
+
+ for (var i = 0; i < queryStringPairs.length; i++) {
+ var pair = queryStringPairs[i].split('=');
+ this.Pairs.push(new KeyValuePair(unescape(pair[0]), unescape(pair[1])))
+ }
+ };
+
+ this.getQueryArgValue = function(key) {
+ for (var i = 0; i < this.Pairs.length; i++) {
+ if (this.Pairs[i].key == key) {
+ return this.Pairs[i].value;
+ }
+ }
+ };
+
+ this.containsQueryArg = function(key) {
+ return this.getQueryArgValue(key);
+ };
+
+ this.indexOf = function(args) {
+ return this.originalUri.indexOf(args);
+ };
+
+ return this;
+};
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs b/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs
index d4e7d2f..564f0e7 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs
@@ -12,6 +12,7 @@ using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using DotNetOpenId.Extensions;
using System.Diagnostics.CodeAnalysis;
+using System.Diagnostics;
namespace DotNetOpenId.RelyingParty
{
@@ -19,8 +20,8 @@ namespace DotNetOpenId.RelyingParty
/// An ASP.NET control providing a complete OpenID login experience.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login")]
- [DefaultProperty("OpenIdUrl")]
- [ToolboxData("<{0}:OpenIdLogin runat=\"server\"></{0}:OpenIdLogin>")]
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdLogin runat=\"server\" />")]
public class OpenIdLogin : OpenIdTextBox
{
Panel panel;
@@ -32,6 +33,7 @@ namespace DotNetOpenId.RelyingParty
Label exampleUrlLabel;
HyperLink registerLink;
CheckBox rememberMeCheckBox;
+ Literal idselectorJavascript;
const short textBoxTabIndexOffset = 0;
const short loginButtonTabIndexOffset = 1;
@@ -164,6 +166,9 @@ namespace DotNetOpenId.RelyingParty
TabIndex = TabIndexDefault;
panel.Controls.Add(table);
+
+ idselectorJavascript = new Literal();
+ panel.Controls.Add(idselectorJavascript);
}
void identifierFormatValidator_ServerValidate(object source, ServerValidateEventArgs args) {
@@ -176,13 +181,32 @@ namespace DotNetOpenId.RelyingParty
}
/// <summary>
+ /// Customizes HTML rendering of the control.
+ /// </summary>
+ protected override void Render(HtmlTextWriter writer) {
+ // avoid writing begin and end SPAN tags for XHTML validity.
+ RenderContents(writer);
+ }
+
+ /// <summary>
/// Renders the child controls.
/// </summary>
protected override void RenderChildren(HtmlTextWriter writer)
{
- if (!this.DesignMode)
+ if (!this.DesignMode) {
label.Attributes["for"] = WrappedTextBox.ClientID;
+ if (!string.IsNullOrEmpty(IdSelectorIdentifier)) {
+ idselectorJavascript.Visible = true;
+ idselectorJavascript.Text = @"<script type='text/javascript'><!--
+idselector_input_id = '" + WrappedTextBox.ClientID + @"';
+// --></script>
+<script type='text/javascript' id='__openidselector' src='https://www.idselector.com/selector/" + IdSelectorIdentifier + @"' charset='utf-8'></script>";
+ } else {
+ idselectorJavascript.Visible = false;
+ }
+ }
+
base.RenderChildren(writer);
}
@@ -439,6 +463,17 @@ namespace DotNetOpenId.RelyingParty
loginButton.ValidationGroup = value;
}
}
+
+ const string idSelectorIdentifierViewStateKey = "IdSelectorIdentifier";
+ /// <summary>
+ /// The unique hash string that ends your idselector.com account.
+ /// </summary>
+ [Category("Behavior")]
+ [Description("The unique hash string that ends your idselector.com account.")]
+ public string IdSelectorIdentifier {
+ get { return (string)(ViewState[idSelectorIdentifierViewStateKey]); }
+ set { ViewState[idSelectorIdentifierViewStateKey] = value; }
+ }
#endregion
#region Properties to hide
@@ -463,7 +498,7 @@ namespace DotNetOpenId.RelyingParty
void loginButton_Click(object sender, EventArgs e)
{
if (!Page.IsValid) return;
- if (OnLoggingIn(Text))
+ if (OnLoggingIn())
LogOn();
}
@@ -480,18 +515,16 @@ namespace DotNetOpenId.RelyingParty
/// <summary>
/// Fires the <see cref="LoggingIn"/> event.
/// </summary>
- /// <param name="userSuppliedIdentifier">
- /// The Identifier supplied by the user via the login page.
- /// </param>
/// <returns>
/// Returns whether the login should proceed. False if some event handler canceled the request.
/// </returns>
- protected virtual bool OnLoggingIn(Identifier userSuppliedIdentifier)
+ protected virtual bool OnLoggingIn()
{
EventHandler<OpenIdEventArgs> loggingIn = LoggingIn;
- PrepareAuthenticationRequest();
+ if (Request == null)
+ CreateRequest();
if (Request != null) {
- OpenIdEventArgs args = new OpenIdEventArgs(Request.ClaimedIdentifier);
+ OpenIdEventArgs args = new OpenIdEventArgs(Request);
if (loggingIn != null)
loggingIn(this, args);
return !args.Cancel;
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdMobileTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdMobileTextBox.cs
index 00c657a..7f6c49d 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdMobileTextBox.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdMobileTextBox.cs
@@ -383,7 +383,7 @@ namespace DotNetOpenId.RelyingParty
Language = RequestLanguage,
TimeZone = RequestTimeZone,
PolicyUrl = string.IsNullOrEmpty(PolicyUrl) ?
- null : new Uri(Page.Request.Url, Page.ResolveUrl(PolicyUrl)),
+ null : new Uri(Util.GetRequestUrlFromContext(), Page.ResolveUrl(PolicyUrl)),
});
}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
index f53b8f0..0cbb4c3 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs
@@ -1,23 +1,99 @@
using System;
-using System.Collections.Specialized;
-using System.Web.SessionState;
-using DotNetOpenId;
-using System.Web;
using System.Collections.Generic;
-using DotNetOpenId.Provider;
-using System.Globalization;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Configuration;
using System.Diagnostics;
+using System.Web;
+using DotNetOpenId.Configuration;
namespace DotNetOpenId.RelyingParty {
/// <summary>
/// Provides the programmatic facilities to act as an OpenId consumer.
/// </summary>
- [DebuggerDisplay("isAuthenticationResponseReady: {isAuthenticationResponseReady}, stateless: {store == null}")]
+ /// <remarks>
+ /// For easier, ASP.NET designer drop-in support for adding OpenID login support,
+ /// see the <see cref="OpenIdLogin"/> or <see cref="OpenIdTextBox"/> controls.
+ /// </remarks>
+ /// <example>
+ /// <code language="ASP.NET">
+ ///&lt;h2&gt;Login Page &lt;/h2&gt;
+ ///&lt;asp:Label ID="Label1" runat="server" Text="OpenID Login" /&gt;
+ ///&lt;asp:TextBox ID="openIdBox" runat="server" /&gt;
+ ///&lt;asp:Button ID="loginButton" runat="server" Text="Login" OnClick="loginButton_Click" /&gt;
+ ///&lt;asp:CustomValidator runat="server" ID="openidValidator" ErrorMessage="Invalid OpenID Identifier"
+ /// ControlToValidate="openIdBox" EnableViewState="false" OnServerValidate="openidValidator_ServerValidate" /&gt;
+ ///&lt;br /&gt;
+ ///&lt;asp:Label ID="loginFailedLabel" runat="server" EnableViewState="False" Text="Login failed"
+ /// Visible="False" /&gt;
+ ///&lt;asp:Label ID="loginCanceledLabel" runat="server" EnableViewState="False" Text="Login canceled"
+ /// Visible="False" /&gt;
+ /// </code>
+ /// <code language="c#">
+ ///protected void openidValidator_ServerValidate(object source, ServerValidateEventArgs args) {
+ /// // This catches common typos that result in an invalid OpenID Identifier.
+ /// args.IsValid = Identifier.IsValid(args.Value);
+ ///}
+ ///
+ ///protected void loginButton_Click(object sender, EventArgs e) {
+ /// if (!Page.IsValid) return; // don't login if custom validation failed.
+ /// OpenIdRelyingParty openid = new OpenIdRelyingParty();
+ /// try {
+ /// IAuthenticationRequest request = openid.CreateRequest(openIdBox.Text);
+ /// // This is where you would add any OpenID extensions you wanted
+ /// // to include in the authentication request.
+ /// // request.AddExtension(someExtensionRequestInstance);
+ ///
+ /// // Send your visitor to their Provider for authentication.
+ /// request.RedirectToProvider();
+ /// } catch (OpenIdException ex) {
+ /// // The user probably entered an Identifier that
+ /// // was not a valid OpenID endpoint.
+ /// openidValidator.Text = ex.Message;
+ /// openidValidator.IsValid = false;
+ /// }
+ ///}
+ ///
+ ///protected void Page_Load(object sender, EventArgs e) {
+ /// openIdBox.Focus();
+ ///
+ /// OpenIdRelyingParty openid = new OpenIdRelyingParty();
+ /// if (openid.Response != null) {
+ /// switch (openid.Response.Status) {
+ /// case AuthenticationStatus.Authenticated:
+ /// // This is where you would look for any OpenID extension responses included
+ /// // in the authentication assertion.
+ /// // var extension = openid.Response.GetExtension&lt;SomeExtensionResponseType&gt;();
+ ///
+ /// // Use FormsAuthentication to tell ASP.NET that the user is now logged in,
+ /// // with the OpenID Claimed Identifier as their username.
+ /// FormsAuthentication.RedirectFromLoginPage(openid.Response.ClaimedIdentifier, false);
+ /// break;
+ /// case AuthenticationStatus.Canceled:
+ /// loginCanceledLabel.Visible = true;
+ /// break;
+ /// case AuthenticationStatus.Failed:
+ /// loginFailedLabel.Visible = true;
+ /// break;
+ /// // We don't need to handle SetupRequired because we're not setting
+ /// // IAuthenticationRequest.Mode to immediate mode.
+ /// //case AuthenticationStatus.SetupRequired:
+ /// // break;
+ /// }
+ /// }
+ ///}
+ /// </code>
+ /// </example>
+ [DebuggerDisplay("isAuthenticationResponseReady: {isAuthenticationResponseReady}, stateless: {Store == null}")]
public class OpenIdRelyingParty {
- IRelyingPartyApplicationStore store;
+ internal IRelyingPartyApplicationStore Store;
Uri request;
IDictionary<string, string> query;
- MessageEncoder encoder;
+ MessageEncoder encoder = new MessageEncoder();
+ internal IDirectMessageChannel DirectMessageChannel = new DirectMessageHttpChannel();
+
+ internal static Uri DefaultRequestUrl { get { return Util.GetRequestUrlFromContext(); } }
+ internal static NameValueCollection DefaultQuery { get { return Util.GetQueryOrFormFromContextNVC(); } }
/// <summary>
/// Constructs an OpenId consumer that uses the current HttpContext request
@@ -27,8 +103,8 @@ namespace DotNetOpenId.RelyingParty {
/// This method requires a current ASP.NET HttpContext.
/// </remarks>
public OpenIdRelyingParty()
- : this(HttpApplicationStore,
- Util.GetRequestUrlFromContext(), Util.GetQueryFromContext()) { }
+ : this(Configuration.Store.CreateInstanceOfStore(HttpApplicationStore),
+ Util.GetRequestUrlFromContext(), Util.GetQueryOrFormFromContext()) { }
/// <summary>
/// Constructs an OpenId consumer that uses a given querystring and IAssociationStore.
/// </summary>
@@ -56,11 +132,16 @@ namespace DotNetOpenId.RelyingParty {
/// which must therefore share the nonce information in the application
/// state store in order to stop the intruder.
/// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIdRelyingParty(IRelyingPartyApplicationStore store, Uri requestUrl, NameValueCollection query) :
this(store, requestUrl, Util.NameValueCollectionToDictionary(query)) {
}
OpenIdRelyingParty(IRelyingPartyApplicationStore store, Uri requestUrl, IDictionary<string, string> query) {
- this.store = store;
+ // Initialize settings with defaults and config section
+ Settings = Configuration.SecuritySettings.CreateSecuritySettings();
+ Settings.RequireSslChanged += new EventHandler(Settings_RequireSslChanged);
+
+ this.Store = store;
if (store != null) {
store.ClearExpiredAssociations(); // every so often we should do this.
}
@@ -69,7 +150,6 @@ namespace DotNetOpenId.RelyingParty {
this.request = requestUrl;
this.query = query;
}
- this.encoder = new MessageEncoder();
}
/// <summary>
@@ -93,9 +173,38 @@ namespace DotNetOpenId.RelyingParty {
/// send to the user agent to initiate the authentication.
/// </returns>
public IAuthenticationRequest CreateRequest(Identifier userSuppliedIdentifier, Realm realm, Uri returnToUrl) {
- return AuthenticationRequest.Create(userSuppliedIdentifier, realm, returnToUrl, store, encoder);
+ if (userSuppliedIdentifier == null) throw new ArgumentNullException("userSuppliedIdentifier");
+ if (realm == null) throw new ArgumentNullException("realm");
+ if (returnToUrl == null) throw new ArgumentNullException("returnToUrl");
+
+ // 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);
+ }
+
+ return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnTo.Uri);
}
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <param name="realm">
+ /// The shorest URL that describes this relying party web site's address.
+ /// For example, if your login page is found at https://www.example.com/login.aspx,
+ /// your realm would typically be https://www.example.com/.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
/// <remarks>
/// This method requires an ASP.NET HttpContext.
/// </remarks>
@@ -103,16 +212,15 @@ namespace DotNetOpenId.RelyingParty {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
// Build the return_to URL
- UriBuilder returnTo = new UriBuilder(HttpContext.Current.Request.Url);
- // Support cookieless sessions by adding the special path if appropriate.
- returnTo.Path = HttpContext.Current.Response.ApplyAppPathModifier(returnTo.Path);
+ UriBuilder returnTo = new UriBuilder(Util.GetRequestUrlFromContext());
// Trim off any parameters with an "openid." prefix, and a few known others
// to avoid carrying state from a prior login attempt.
returnTo.Query = string.Empty;
- var returnToParams = new Dictionary<string, string>(HttpContext.Current.Request.QueryString.Count);
- foreach (string key in HttpContext.Current.Request.QueryString) {
+ NameValueCollection queryParams = Util.GetQueryFromContextNVC();
+ var returnToParams = new Dictionary<string, string>(queryParams.Count);
+ foreach (string key in queryParams) {
if (!ShouldParameterBeStrippedFromReturnToUrl(key)) {
- returnToParams.Add(key, HttpContext.Current.Request.QueryString[key]);
+ returnToParams.Add(key, queryParams[key]);
}
}
UriUtil.AppendQueryArgs(returnTo, returnToParams);
@@ -126,6 +234,17 @@ namespace DotNetOpenId.RelyingParty {
|| parameterName == Token.TokenKey;
}
+ /// <summary>
+ /// Creates an authentication request to verify that a user controls
+ /// some given Identifier.
+ /// </summary>
+ /// <param name="userSuppliedIdentifier">
+ /// The Identifier supplied by the user. This may be a URL, an XRI or i-name.
+ /// </param>
+ /// <returns>
+ /// An authentication request object that describes the HTTP response to
+ /// send to the user agent to initiate the authentication.
+ /// </returns>
/// <remarks>
/// This method requires an ASP.NET HttpContext.
/// </remarks>
@@ -133,11 +252,18 @@ namespace DotNetOpenId.RelyingParty {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
// Build the realm URL
- UriBuilder realmUrl = new UriBuilder(HttpContext.Current.Request.Url);
+ UriBuilder realmUrl = new UriBuilder(Util.GetRequestUrlFromContext());
realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
realmUrl.Query = null;
realmUrl.Fragment = null;
+ // For RP discovery, the realm url MUST NOT redirect. To prevent this for
+ // virtual directory hosted apps, we need to make sure that the realm path ends
+ // in a slash (since our calculation above guarantees it doesn't end in a specific
+ // page like default.aspx).
+ if (!realmUrl.Path.EndsWith("/", StringComparison.Ordinal))
+ realmUrl.Path += "/";
+
return CreateRequest(userSuppliedIdentifier, new Realm(realmUrl.Uri));
}
@@ -160,14 +286,12 @@ namespace DotNetOpenId.RelyingParty {
/// Gets the result of a user agent's visit to his OpenId provider in an
/// authentication attempt. Null if no response is available.
/// </summary>
- [DebuggerBrowsable(DebuggerBrowsableState.Never)] // getter does work
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)] // getter does lots of processing, so avoid debugger calling it.
public IAuthenticationResponse Response {
get {
if (response == null && isAuthenticationResponseReady) {
try {
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("OpenID authentication response detected.");
- response = AuthenticationResponse.Parse(query, store, request);
+ response = AuthenticationResponse.Parse(query, this, request, true);
} catch (OpenIdException ex) {
response = new FailedAuthenticationResponse(ex);
}
@@ -176,11 +300,111 @@ namespace DotNetOpenId.RelyingParty {
}
}
+ /// <summary>
+ /// The message encoder to use.
+ /// </summary>
+ internal MessageEncoder Encoder { get { return encoder; } }
+
+ private Comparison<IXrdsProviderEndpoint> endpointOrder = DefaultEndpointOrder;
+ /// <summary>
+ /// Gets/sets the ordering routine that will determine which XRDS
+ /// Service element to try first
+ /// </summary>
+ /// <remarks>
+ /// This may never be null. To reset to default behavior this property
+ /// can be set to the value of <see cref="DefaultEndpointOrder"/>.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public Comparison<IXrdsProviderEndpoint> EndpointOrder {
+ get { return endpointOrder; }
+ set {
+ if (value == null) throw new ArgumentNullException("value");
+ endpointOrder = value;
+ }
+ }
+ /// <summary>
+ /// Gets an XRDS sorting routine that uses the XRDS Service/@Priority
+ /// attribute to determine order.
+ /// </summary>
+ /// <remarks>
+ /// Endpoints lacking any priority value are sorted to the end of the list.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
+ get {
+ // Sort first by service type (OpenID 2.0, 1.1, 1.0),
+ // then by Service/@priority, then by Service/Uri/@priority
+ return (se1, se2) => {
+ int result = getEndpointPrecedenceOrderByServiceType(se1).CompareTo(getEndpointPrecedenceOrderByServiceType(se2));
+ if (result != 0) return result;
+ if (se1.ServicePriority.HasValue && se2.ServicePriority.HasValue) {
+ result = se1.ServicePriority.Value.CompareTo(se2.ServicePriority.Value);
+ if (result != 0) return result;
+ if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) {
+ return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value);
+ } else if (se1.UriPriority.HasValue) {
+ return -1;
+ } else if (se2.UriPriority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else {
+ if (se1.ServicePriority.HasValue) {
+ return -1;
+ } else if (se2.ServicePriority.HasValue) {
+ return 1;
+ } else {
+ // neither service defines a priority, so base ordering by uri priority.
+ if (se1.UriPriority.HasValue && se2.UriPriority.HasValue) {
+ return se1.UriPriority.Value.CompareTo(se2.UriPriority.Value);
+ } else if (se1.UriPriority.HasValue) {
+ return -1;
+ } else if (se2.UriPriority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+ };
+ }
+ }
+
+ static double getEndpointPrecedenceOrderByServiceType(IXrdsProviderEndpoint endpoint) {
+ // The numbers returned from this method only need to compare against other numbers
+ // from this method, which makes them arbitrary but relational to only others here.
+ if (endpoint.IsTypeUriPresent(Protocol.v20.OPIdentifierServiceTypeURI)) {
+ return 0;
+ }
+ if (endpoint.IsTypeUriPresent(Protocol.v20.ClaimedIdentifierServiceTypeURI)) {
+ return 1;
+ }
+ if (endpoint.IsTypeUriPresent(Protocol.v11.ClaimedIdentifierServiceTypeURI)) {
+ return 2;
+ }
+ if (endpoint.IsTypeUriPresent(Protocol.v10.ClaimedIdentifierServiceTypeURI)) {
+ return 3;
+ }
+ return 10;
+ }
+
+ /// <summary>
+ /// Provides a way to optionally filter the providers that may be used in authenticating a user.
+ /// </summary>
+ /// <remarks>
+ /// If provided, the delegate should return true to accept an endpoint, and false to reject it.
+ /// If null, all identity providers will be accepted. This is the default.
+ /// </remarks>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
+ public EndpointSelector EndpointFilter { get; set; }
+
const string associationStoreKey = "DotNetOpenId.RelyingParty.RelyingParty.AssociationStore";
/// <summary>
/// The standard state storage mechanism that uses ASP.NET's HttpApplication state dictionary
/// to store associations and nonces.
/// </summary>
+ [EditorBrowsable(EditorBrowsableState.Advanced)]
public static IRelyingPartyApplicationStore HttpApplicationStore {
get {
HttpContext context = HttpContext.Current;
@@ -200,5 +424,34 @@ namespace DotNetOpenId.RelyingParty {
return store;
}
}
+
+ /// <summary>
+ /// Provides access to the adjustable security settings of this instance
+ /// of <see cref="OpenIdRelyingParty"/>.
+ /// </summary>
+ public RelyingPartySecuritySettings Settings { get; private set; }
+
+ void Settings_RequireSslChanged(object sender, EventArgs e) {
+ // reset response that may have been calculated to force
+ // reconsideration with new security policy.
+ response = null;
+ }
+
+ /// <summary>
+ /// Gets the relevant Configuration section for this OpenIdRelyingParty.
+ /// </summary>
+ internal static RelyingPartySection Configuration {
+ get { return RelyingPartySection.Configuration; }
+ }
}
+
+ /// <summary>
+ /// A delegate that decides whether a given OpenID Provider endpoint may be
+ /// considered for authenticating a user.
+ /// </summary>
+ /// <returns>
+ /// True if the endpoint should be considered.
+ /// False to remove it from the pool of acceptable providers.
+ /// </returns>
+ public delegate bool EndpointSelector(IXrdsProviderEndpoint endpoint);
}
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs
index 57a5fd4..55861d4 100644
--- a/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs
+++ b/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs
@@ -32,9 +32,9 @@ namespace DotNetOpenId.RelyingParty
/// control, but requires more work to be done by the hosting web site to
/// assemble a complete login experience.
/// </remarks>
- [DefaultProperty("Text")]
- [ToolboxData("<{0}:OpenIdTextBox runat=\"server\"></{0}:OpenIdTextBox>")]
- public class OpenIdTextBox : CompositeControl
+ [DefaultProperty("Text"), ValidationProperty("Text")]
+ [ToolboxData("<{0}:OpenIdTextBox runat=\"server\" />")]
+ public class OpenIdTextBox : CompositeControl, IEditableTextControl, ITextControl
{
/// <summary>
/// Instantiates an <see cref="OpenIdTextBox"/> instance.
@@ -124,7 +124,7 @@ namespace DotNetOpenId.RelyingParty
/// <summary>
/// The OpenID <see cref="Realm"/> of the relying party web site.
/// </summary>
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.Realm"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings"), SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")]
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.Realm"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings"), SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")]
[Bindable(true)]
[Category(behaviorCategory)]
[DefaultValue(realmUrlDefault)]
@@ -137,7 +137,7 @@ namespace DotNetOpenId.RelyingParty
if (Page != null && !DesignMode)
{
// Validate new value by trying to construct a Realm object based on it.
- new Realm(getResolvedRealm(value)); // throws an exception on failure.
+ new Realm(Util.GetResolvedRealm(Page, value)); // throws an exception on failure.
}
else
{
@@ -157,6 +157,36 @@ namespace DotNetOpenId.RelyingParty
}
}
+ const string returnToUrlViewStateKey = "ReturnToUrl";
+ const string returnToUrlDefault = "";
+ /// <summary>
+ /// The OpenID ReturnTo of the relying party web site.
+ /// </summary>
+ [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "System.Uri"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.ReturnTo"), System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings"), SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")]
+ [Bindable(true)]
+ [Category(behaviorCategory)]
+ [DefaultValue(returnToUrlDefault)]
+ [Description("The OpenID ReturnTo of the relying party web site.")]
+ public string ReturnToUrl {
+ get { return (string)(ViewState[returnToUrlViewStateKey] ?? returnToUrlDefault); }
+ set {
+ if (Page != null && !DesignMode) {
+ // Validate new value by trying to construct a Uri based on it.
+ new Uri(Util.GetRequestUrlFromContext(), Page.ResolveUrl(value)); // throws an exception on failure.
+ } else {
+ // We can't fully test it, but it should start with either ~/ or a protocol.
+ if (Regex.IsMatch(value, @"^https?://")) {
+ new Uri(value); // make sure it's fully-qualified, but ignore wildcards
+ } else if (value.StartsWith("~/", StringComparison.Ordinal)) {
+ // this is valid too
+ } else {
+ throw new UriFormatException();
+ }
+ }
+ ViewState[returnToUrlViewStateKey] = value;
+ }
+ }
+
const string immediateModeViewStateKey = "ImmediateMode";
const bool immediateModeDefault = false;
/// <summary>
@@ -179,6 +209,20 @@ namespace DotNetOpenId.RelyingParty
set { ViewState[immediateModeViewStateKey] = value; }
}
+ const string statelessViewStateKey = "Stateless";
+ const bool statelessDefault = false;
+ /// <summary>
+ /// Controls whether stateless mode is used.
+ /// </summary>
+ [Bindable(true)]
+ [Category(behaviorCategory)]
+ [DefaultValue(statelessDefault)]
+ [Description("Controls whether stateless mode is used.")]
+ public bool Stateless {
+ get { return (bool)(ViewState[statelessViewStateKey] ?? statelessDefault); }
+ set { ViewState[statelessViewStateKey] = value; }
+ }
+
const string cssClassDefault = "openid";
/// <summary>
/// Gets/sets the CSS class assigned to the text box.
@@ -207,6 +251,7 @@ namespace DotNetOpenId.RelyingParty
set { ViewState[showLogoViewStateKey] = value; }
}
+ const string usePersistentCookieCallbackKey = "OpenIdTextBox_UsePersistentCookie";
const string usePersistentCookieViewStateKey = "UsePersistentCookie";
/// <summary>
/// Default value of <see cref="UsePersistentCookie"/>.
@@ -241,6 +286,19 @@ namespace DotNetOpenId.RelyingParty
set { WrappedTextBox.Columns = value; }
}
+ const int maxLengthDefault = 40;
+ /// <summary>
+ /// Gets or sets the maximum number of characters the browser should allow
+ /// </summary>
+ [Bindable(true)]
+ [Category(appearanceCategory)]
+ [DefaultValue(maxLengthDefault)]
+ [Description("The maximum number of characters the browser should allow.")]
+ public int MaxLength {
+ get { return WrappedTextBox.MaxLength; }
+ set { WrappedTextBox.MaxLength = value; }
+ }
+
/// <summary>
/// Default value for <see cref="TabIndex"/> property.
/// </summary>
@@ -443,6 +501,30 @@ namespace DotNetOpenId.RelyingParty
get { return (bool)(ViewState[enableRequestProfileViewStateKey] ?? enableRequestProfileDefault); }
set { ViewState[enableRequestProfileViewStateKey] = value; }
}
+
+ const string requireSslViewStateKey = "RequireSsl";
+ const bool requireSslDefault = false;
+ /// <summary>
+ /// Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.
+ /// </summary>
+ [Bindable(true)]
+ [Category(behaviorCategory)]
+ [DefaultValue(requireSslDefault)]
+ [Description("Turns on high security mode, requiring the full authentication pipeline to be protected by SSL.")]
+ public bool RequireSsl {
+ get { return (bool)(ViewState[requireSslViewStateKey] ?? requireSslDefault); }
+ set { ViewState[requireSslViewStateKey] = value; }
+ }
+
+ /// <summary>
+ /// A custom application store to use, or null to use the default.
+ /// </summary>
+ /// <remarks>
+ /// If set, this property must be set in each Page Load event
+ /// as it is not persisted across postbacks.
+ /// </remarks>
+ public IRelyingPartyApplicationStore CustomApplicationStore { get; set; }
+
#endregion
#region Properties to hide
@@ -553,8 +635,14 @@ namespace DotNetOpenId.RelyingParty
base.OnLoad(e);
if (!Enabled || Page.IsPostBack) return;
- var consumer = new OpenIdRelyingParty();
+ var consumer = createRelyingParty();
if (consumer.Response != null) {
+ string persistentString = consumer.Response.GetCallbackArgument(usePersistentCookieCallbackKey);
+ bool persistentBool;
+ if (persistentString != null && bool.TryParse(persistentString, out persistentBool)) {
+ UsePersistentCookie = persistentBool;
+ }
+
switch (consumer.Response.Status) {
case AuthenticationStatus.Canceled:
OnCanceled(consumer.Response);
@@ -574,6 +662,23 @@ namespace DotNetOpenId.RelyingParty
}
}
+ private OpenIdRelyingParty createRelyingParty() {
+ // If we're in stateful mode, first use the explicitly given one on this control if there
+ // is one. Then try the configuration file specified one. Finally, use the default
+ // in-memory one that's built into OpenIdRelyingParty.
+ IRelyingPartyApplicationStore store = Stateless ? null :
+ (CustomApplicationStore ?? OpenIdRelyingParty.Configuration.Store.CreateInstanceOfStore(OpenIdRelyingParty.HttpApplicationStore));
+ Uri request = OpenIdRelyingParty.DefaultRequestUrl;
+ NameValueCollection query = OpenIdRelyingParty.DefaultQuery;
+ var rp = new OpenIdRelyingParty(store, request, query);
+ // Only set RequireSsl to true, as we don't want to override
+ // a .config setting of true with false.
+ if (RequireSsl) {
+ rp.Settings.RequireSsl = true;
+ }
+ return rp;
+ }
+
/// <summary>
/// Prepares the text box to be rendered.
/// </summary>
@@ -585,7 +690,7 @@ namespace DotNetOpenId.RelyingParty
string logoUrl = Page.ClientScript.GetWebResourceUrl(
typeof(OpenIdTextBox), EmbeddedLogoResourceName);
WrappedTextBox.Style["background"] = string.Format(CultureInfo.InvariantCulture,
- "url({0}) no-repeat", logoUrl);
+ "#fff url({0}) no-repeat", HttpUtility.HtmlEncode(logoUrl));
WrappedTextBox.Style["background-position"] = "0 50%";
WrappedTextBox.Style[HtmlTextWriterStyle.PaddingLeft] = "18px";
WrappedTextBox.Style[HtmlTextWriterStyle.BorderStyle] = "solid";
@@ -599,32 +704,64 @@ namespace DotNetOpenId.RelyingParty
/// </summary>
protected IAuthenticationRequest Request;
/// <summary>
- /// Constructs the authentication request and adds the Simple Registration extension arguments.
+ /// Constructs the authentication request and returns it.
/// </summary>
+ /// <remarks>
+ /// <para>This method need not be called before calling the <see cref="LogOn"/> method,
+ /// but is offered in the event that adding extensions to the request is desired.</para>
+ /// <para>The Simple Registration extension arguments are added to the request
+ /// before returning if <see cref="EnableRequestProfile"/> is set to true.</para>
+ /// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
- protected void PrepareAuthenticationRequest() {
+ public IAuthenticationRequest CreateRequest() {
+ if (Request != null)
+ throw new InvalidOperationException(Strings.CreateRequestAlreadyCalled);
if (string.IsNullOrEmpty(Text))
throw new InvalidOperationException(DotNetOpenId.Strings.OpenIdTextBoxEmpty);
try {
- var consumer = new OpenIdRelyingParty();
+ var consumer = createRelyingParty();
+
+ // Approximate the returnTo (either based on the customize property or the page URL)
+ // so we can use it to help with Realm resolution.
+ Uri returnToApproximation = ReturnToUrl != null ? new Uri(Util.GetRequestUrlFromContext(), ReturnToUrl) : Page.Request.Url;
// Resolve the trust root, and swap out the scheme and port if necessary to match the
// return_to URL, since this match is required by OpenId, and the consumer app
// may be using HTTP at some times and HTTPS at others.
- UriBuilder realm = getResolvedRealm(RealmUrl);
- realm.Scheme = Page.Request.Url.Scheme;
- realm.Port = Page.Request.Url.Port;
+ UriBuilder realm = Util.GetResolvedRealm(Page, RealmUrl);
+ realm.Scheme = returnToApproximation.Scheme;
+ realm.Port = returnToApproximation.Port;
// Initiate openid request
- Request = consumer.CreateRequest(Text, new Realm(realm));
- Request.Mode = ImmediateMode ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup;
- if (EnableRequestProfile) addProfileArgs(Request);
+ // We use TryParse here to avoid throwing an exception which
+ // might slip through our validator control if it is disabled.
+ Identifier userSuppliedIdentifier;
+ if (Identifier.TryParse(Text, out userSuppliedIdentifier)) {
+ Realm typedRealm = new Realm(realm);
+ if (string.IsNullOrEmpty(ReturnToUrl)) {
+ Request = consumer.CreateRequest(userSuppliedIdentifier, typedRealm);
+ } else {
+ // Since the user actually gave us a return_to value,
+ // the "approximation" is exactly what we want.
+ Request = consumer.CreateRequest(userSuppliedIdentifier, typedRealm, returnToApproximation);
+ }
+ Request.Mode = ImmediateMode ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup;
+ if (EnableRequestProfile) addProfileArgs(Request);
+
+ // Add state that needs to survive across the redirect.
+ Request.AddCallbackArguments(usePersistentCookieCallbackKey, UsePersistentCookie.ToString(CultureInfo.InvariantCulture));
+ } else {
+ Logger.WarnFormat("An invalid identifier was entered ({0}), but not caught by any validation routine.", Text);
+ Request = null;
+ }
} catch (WebException ex) {
OnFailed(new FailedAuthenticationResponse(ex));
} catch (OpenIdException ex) {
OnFailed(new FailedAuthenticationResponse(ex));
}
+
+ return Request;
}
/// <summary>
@@ -635,7 +772,7 @@ namespace DotNetOpenId.RelyingParty
public void LogOn()
{
if (Request == null)
- PrepareAuthenticationRequest();
+ CreateRequest();
if (Request != null)
Request.RedirectToProvider();
}
@@ -653,41 +790,10 @@ namespace DotNetOpenId.RelyingParty
Language = RequestLanguage,
TimeZone = RequestTimeZone,
PolicyUrl = string.IsNullOrEmpty(PolicyUrl) ?
- null : new Uri(Page.Request.Url, Page.ResolveUrl(PolicyUrl)),
+ null : new Uri(Util.GetRequestUrlFromContext(), Page.ResolveUrl(PolicyUrl)),
});
}
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.Realm")]
- UriBuilder getResolvedRealm(string realm)
- {
- Debug.Assert(Page != null, "Current HttpContext required to resolve URLs.");
- // Allow for *. realm notation, as well as ASP.NET ~/ shortcuts.
-
- // We have to temporarily remove the *. notation if it's there so that
- // the rest of our URL manipulation will succeed.
- bool foundWildcard = false;
- // Note: we don't just use string.Replace because poorly written URLs
- // could potentially have multiple :// sequences in them.
- string realmNoWildcard = Regex.Replace(realm, @"^(\w+://)\*\.",
- delegate(Match m) {
- foundWildcard = true;
- return m.Groups[1].Value;
- });
-
- UriBuilder fullyQualifiedRealm = new UriBuilder(
- new Uri(Page.Request.Url, Page.ResolveUrl(realmNoWildcard)));
-
- if (foundWildcard)
- {
- fullyQualifiedRealm.Host = "*." + fullyQualifiedRealm.Host;
- }
-
- // Is it valid?
- new Realm(fullyQualifiedRealm); // throws if not valid
-
- return fullyQualifiedRealm;
- }
-
#region Events
/// <summary>
/// Fired upon completion of a successful login.
@@ -764,6 +870,18 @@ namespace DotNetOpenId.RelyingParty
}
#endregion
+
+ #region IEditableTextControl Members
+
+ /// <summary>
+ /// Occurs when the content of the text box changes between posts to the server.
+ /// </summary>
+ public event EventHandler TextChanged {
+ add { WrappedTextBox.TextChanged += value; }
+ remove { WrappedTextBox.TextChanged -= value; }
+ }
+
+ #endregion
}
/// <summary>
@@ -774,8 +892,11 @@ namespace DotNetOpenId.RelyingParty
/// Constructs an object with minimal information of an incomplete or failed
/// authentication attempt.
/// </summary>
- internal OpenIdEventArgs(Identifier claimedIdentifier) {
- ClaimedIdentifier = claimedIdentifier;
+ internal OpenIdEventArgs(IAuthenticationRequest request) {
+ if (request == null) throw new ArgumentNullException("request");
+ Request = request;
+ ClaimedIdentifier = request.ClaimedIdentifier;
+ IsDirectedIdentity = request.IsDirectedIdentity;
}
/// <summary>
/// Constructs an object with information on a completed authentication attempt
@@ -785,25 +906,30 @@ namespace DotNetOpenId.RelyingParty
if (response == null) throw new ArgumentNullException("response");
Response = response;
ClaimedIdentifier = response.ClaimedIdentifier;
- ProfileFields = response.GetExtension<ClaimsResponse>();
}
/// <summary>
/// Cancels the OpenID authentication and/or login process.
/// </summary>
public bool Cancel { get; set; }
/// <summary>
- /// The Identifier the user is claiming to own.
+ /// The Identifier the user is claiming to own. Or null if the user
+ /// is using Directed Identity.
/// </summary>
public Identifier ClaimedIdentifier { get; private set; }
+ /// <summary>
+ /// Whether the user has selected to let his Provider determine
+ /// the ClaimedIdentifier to use as part of successful authentication.
+ /// </summary>
+ public bool IsDirectedIdentity { get; private set; }
/// <summary>
- /// Gets the details of the OpenId authentication response.
+ /// Gets the details of the OpenID authentication request,
+ /// and allows for adding extensions.
/// </summary>
- public IAuthenticationResponse Response { get; private set; }
+ public IAuthenticationRequest Request { get; private set; }
/// <summary>
- /// Gets the simple registration (sreg) extension fields given
- /// by the provider, if any.
+ /// Gets the details of the OpenID authentication response.
/// </summary>
- public ClaimsResponse ProfileFields { get; private set; }
+ public IAuthenticationResponse Response { get; private set; }
}
}
diff --git a/src/DotNetOpenId/RelyingParty/RelyingPartySecuritySettings.cs b/src/DotNetOpenId/RelyingParty/RelyingPartySecuritySettings.cs
new file mode 100644
index 0000000..236d6e2
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/RelyingPartySecuritySettings.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.RelyingParty {
+ /// <summary>
+ /// Security settings that are applicable to relying parties.
+ /// </summary>
+ public sealed class RelyingPartySecuritySettings : SecuritySettings {
+ internal RelyingPartySecuritySettings() : base(false) { }
+
+ private bool requireSsl;
+ /// <summary>
+ /// Gets/sets whether the entire pipeline from Identifier discovery to Provider redirect
+ /// is guaranteed to be encrypted using HTTPS for authentication to succeed.
+ /// </summary>
+ /// <remarks>
+ /// <para>Setting this property to true is appropriate for RPs with highly sensitive
+ /// personal information behind the authentication (money management, health records, etc.)</para>
+ /// <para>When set to true, some behavioral changes and additional restrictions are placed:</para>
+ /// <list>
+ /// <item>User-supplied identifiers lacking a scheme are prepended with
+ /// HTTPS:// rather than the standard HTTP:// automatically.</item>
+ /// <item>User-supplied identifiers are not allowed to use HTTP for the scheme.</item>
+ /// <item>All redirects during discovery on the user-supplied identifier must be HTTPS.</item>
+ /// <item>Any XRDS file found by discovery on the User-supplied identifier must be protected using HTTPS.</item>
+ /// <item>Only Provider endpoints found at HTTPS URLs will be considered.</item>
+ /// <item>If the discovered identifier is an OP Identifier (directed identity), the
+ /// Claimed Identifier eventually asserted by the Provider must be an HTTPS identifier.</item>
+ /// <item>In the case of an unsolicited assertion, the asserted Identifier, discovery on it and
+ /// the asserting provider endpoint must all be secured by HTTPS.</item>
+ /// </list>
+ /// <para>Although the first redirect from this relying party to the Provider is required
+ /// to use HTTPS, any additional redirects within the Provider cannot be protected and MAY
+ /// revert the user's connection to HTTP, based on individual Provider implementation.
+ /// There is nothing that the RP can do to detect or prevent this.</para>
+ /// <para>
+ /// An <see cref="OpenIdException"/> is thrown when a secure pipeline cannot be established.
+ /// </para>
+ /// </remarks>
+ public bool RequireSsl {
+ get { return requireSsl; }
+ set {
+ if (requireSsl == value) return;
+ requireSsl = value;
+ OnRequireSslChanged();
+ }
+ }
+
+ internal event EventHandler RequireSslChanged;
+ /// <summary>
+ /// Fires the <see cref="RequireSslChanged"/> event.
+ /// </summary>
+ void OnRequireSslChanged() {
+ EventHandler requireSslChanged = RequireSslChanged;
+ if (requireSslChanged != null) {
+ requireSslChanged(this, new EventArgs());
+ }
+ }
+
+ /// <summary>
+ /// Gets/sets the oldest version of OpenID the remote party is allowed to implement.
+ /// </summary>
+ /// <value>Defaults to <see cref="ProtocolVersion.V10"/></value>
+ public ProtocolVersion MinimumRequiredOpenIdVersion { get; set; }
+ }
+}
diff --git a/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs b/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs
index 98c2e9a..b5ede55 100644
--- a/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs
+++ b/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs
@@ -7,13 +7,15 @@ using System.Xml.XPath;
using System.IO;
using DotNetOpenId.Yadis;
using System.Diagnostics;
+using DotNetOpenId.Extensions;
+using System.Globalization;
namespace DotNetOpenId.RelyingParty {
/// <summary>
/// Represents information discovered about a user-supplied Identifier.
/// </summary>
[DebuggerDisplay("ClaimedIdentifier: {ClaimedIdentifier}, ProviderEndpoint: {ProviderEndpoint}, OpenId: {Protocol.Version}")]
- internal class ServiceEndpoint {
+ internal class ServiceEndpoint : IXrdsProviderEndpoint {
/// <summary>
/// The URL which accepts OpenID Authentication protocol messages.
/// </summary>
@@ -22,11 +24,19 @@ namespace DotNetOpenId.RelyingParty {
/// This value MUST be an absolute HTTP or HTTPS URL.
/// </remarks>
public Uri ProviderEndpoint { get; private set; }
+ /// <summary>
+ /// Returns true if the <see cref="ProviderEndpoint"/> is using an encrypted channel.
+ /// </summary>
+ internal bool IsSecure {
+ get { return string.Equals(ProviderEndpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase); }
+ }
+ Uri IProviderEndpoint.Uri { get { return ProviderEndpoint; } }
/*
/// <summary>
/// An Identifier for an OpenID Provider.
/// </summary>
public Identifier ProviderIdentifier { get; private set; }
+ */
/// <summary>
/// An Identifier that was presented by the end user to the Relying Party,
/// or selected by the user at the OpenID Provider.
@@ -35,7 +45,7 @@ namespace DotNetOpenId.RelyingParty {
/// is used, the OP may then assist the end user in selecting an Identifier
/// to share with the Relying Party.
/// </summary>
- public Identifier UserSuppliedIdentifier { get; private set; }*/
+ public Identifier UserSuppliedIdentifier { get; private set; }
/// <summary>
/// The Identifier that the end user claims to own.
/// </summary>
@@ -46,30 +56,99 @@ namespace DotNetOpenId.RelyingParty {
/// control.
/// </summary>
public Identifier ProviderLocalIdentifier { get; private set; }
+ string friendlyIdentifierForDisplay;
+ /// <summary>
+ /// Supports the <see cref="IAuthenticationResponse.FriendlyIdentifierForDisplay"/> property.
+ /// </summary>
+ public string FriendlyIdentifierForDisplay {
+ get {
+ if (friendlyIdentifierForDisplay == null) {
+ XriIdentifier xri = ClaimedIdentifier as XriIdentifier;
+ UriIdentifier uri = ClaimedIdentifier as UriIdentifier;
+ if (xri != null) {
+ if (UserSuppliedIdentifier == null || String.Equals(UserSuppliedIdentifier, ClaimedIdentifier, StringComparison.OrdinalIgnoreCase)) {
+ friendlyIdentifierForDisplay = ClaimedIdentifier;
+ } else {
+ friendlyIdentifierForDisplay = UserSuppliedIdentifier;
+ }
+ } else if (uri != null) {
+ if (uri != Protocol.ClaimedIdentifierForOPIdentifier) {
+ string displayUri = uri.Uri.Authority + uri.Uri.PathAndQuery;
+ displayUri = displayUri.TrimEnd('/');
+ // Multi-byte unicode characters get encoded by the Uri class for transit.
+ // Since this is for display purposes, we want to reverse this and display a readable
+ // representation of these foreign characters.
+ friendlyIdentifierForDisplay = Uri.UnescapeDataString(displayUri);
+ }
+ } else {
+ Debug.Fail("Doh! We never should have reached here.");
+ friendlyIdentifierForDisplay = ClaimedIdentifier;
+ }
+ }
+ return friendlyIdentifierForDisplay;
+ }
+ }
/// <summary>
/// Gets the list of services available at this OP Endpoint for the
- /// claimed Identifier.
+ /// claimed Identifier. May be null.
/// </summary>
public string[] ProviderSupportedServiceTypeUris { get; private set; }
- internal ServiceEndpoint(Identifier claimedIdentifier, Uri providerEndpoint,
- Identifier providerLocalIdentifier, string[] providerSupportedServiceTypeUris) {
+ ServiceEndpoint(Identifier claimedIdentifier, Identifier userSuppliedIdentifier,
+ Uri providerEndpoint, Identifier providerLocalIdentifier,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
if (claimedIdentifier == null) throw new ArgumentNullException("claimedIdentifier");
if (providerEndpoint == null) throw new ArgumentNullException("providerEndpoint");
if (providerSupportedServiceTypeUris == null) throw new ArgumentNullException("providerSupportedServiceTypeUris");
ClaimedIdentifier = claimedIdentifier;
+ UserSuppliedIdentifier = userSuppliedIdentifier;
ProviderEndpoint = providerEndpoint;
ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier;
ProviderSupportedServiceTypeUris = providerSupportedServiceTypeUris;
+ this.servicePriority = servicePriority;
+ this.uriPriority = uriPriority;
}
- ServiceEndpoint(Identifier claimedIdentifier, Uri providerEndpoint,
- Identifier providerLocalIdentifier, Protocol protocol) {
+ /// <summary>
+ /// Used for deserializing <see cref="ServiceEndpoint"/> from authentication responses.
+ /// </summary>
+ ServiceEndpoint(Identifier claimedIdentifier, Identifier userSuppliedIdentifier,
+ Uri providerEndpoint, Identifier providerLocalIdentifier, Protocol protocol) {
ClaimedIdentifier = claimedIdentifier;
+ UserSuppliedIdentifier = userSuppliedIdentifier;
ProviderEndpoint = providerEndpoint;
ProviderLocalIdentifier = providerLocalIdentifier ?? claimedIdentifier;
this.protocol = protocol;
}
+ internal static ServiceEndpoint CreateForProviderIdentifier(
+ Identifier providerIdentifier, Uri providerEndpoint,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+
+ Protocol protocol = Protocol.Detect(providerSupportedServiceTypeUris);
+
+ return new ServiceEndpoint(protocol.ClaimedIdentifierForOPIdentifier, providerIdentifier,
+ providerEndpoint, protocol.ClaimedIdentifierForOPIdentifier,
+ providerSupportedServiceTypeUris, servicePriority, uriPriority);
+ }
+
+ internal static ServiceEndpoint CreateForClaimedIdentifier(
+ Identifier claimedIdentifier, Identifier providerLocalIdentifier,
+ Uri providerEndpoint,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+
+ return CreateForClaimedIdentifier(claimedIdentifier, null, providerLocalIdentifier,
+ providerEndpoint, providerSupportedServiceTypeUris, servicePriority, uriPriority);
+ }
+
+ internal static ServiceEndpoint CreateForClaimedIdentifier(
+ Identifier claimedIdentifier, Identifier userSuppliedIdentifier, Identifier providerLocalIdentifier,
+ Uri providerEndpoint,
+ string[] providerSupportedServiceTypeUris, int? servicePriority, int? uriPriority) {
+
+ return new ServiceEndpoint(claimedIdentifier, userSuppliedIdentifier, providerEndpoint,
+ providerLocalIdentifier, providerSupportedServiceTypeUris, servicePriority, uriPriority);
+ }
+
Protocol protocol;
/// <summary>
/// Gets the OpenID protocol used by the Provider.
@@ -77,21 +156,58 @@ namespace DotNetOpenId.RelyingParty {
public Protocol Protocol {
get {
if (protocol == null) {
- protocol =
- Util.FindBestVersion(p => p.OPIdentifierServiceTypeURI, ProviderSupportedServiceTypeUris) ??
- Util.FindBestVersion(p => p.ClaimedIdentifierServiceTypeURI, ProviderSupportedServiceTypeUris);
+ protocol = Protocol.Detect(ProviderSupportedServiceTypeUris);
}
if (protocol != null) return protocol;
throw new InvalidOperationException("Unable to determine the version of OpenID the Provider supports.");
}
}
+ public bool IsTypeUriPresent(string typeUri) {
+ return IsExtensionSupported(typeUri);
+ }
+
public bool IsExtensionSupported(string extensionUri) {
if (ProviderSupportedServiceTypeUris == null)
throw new InvalidOperationException("Cannot lookup extension support on a rehydrated ServiceEndpoint.");
return Array.IndexOf(ProviderSupportedServiceTypeUris, extensionUri) >= 0;
}
+ public bool IsExtensionSupported(IExtension extension) {
+ if (extension == null) throw new ArgumentNullException("extension");
+
+ // Consider the primary case.
+ if (IsExtensionSupported(extension.TypeUri)) {
+ return true;
+ }
+ // Consider the secondary cases.
+ if (extension.AdditionalSupportedTypeUris != null) {
+ foreach (string extensionTypeUri in extension.AdditionalSupportedTypeUris) {
+ if (IsExtensionSupported(extensionTypeUri)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public bool IsExtensionSupported<T>() where T : Extensions.IExtension, new() {
+ T extension = new T();
+ return IsExtensionSupported(extension);
+ }
+
+ public bool IsExtensionSupported(Type extensionType) {
+ if (extensionType == null) throw new ArgumentNullException("extensionType");
+ if (!typeof(Extensions.IExtension).IsAssignableFrom(extensionType))
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
+ Strings.TypeMustImplementX, typeof(Extensions.IExtension).FullName),
+ "extensionType");
+ var extension = (Extensions.IExtension)Activator.CreateInstance(extensionType);
+ return IsExtensionSupported(extension);
+ }
+
+ Version IProviderEndpoint.Version { get { return Protocol.Version; } }
+
/// <summary>
/// Saves the discovered information about this endpoint
/// for later comparison to validate assertions.
@@ -99,8 +215,10 @@ namespace DotNetOpenId.RelyingParty {
internal void Serialize(TextWriter writer) {
writer.WriteLine(ClaimedIdentifier);
writer.WriteLine(ProviderLocalIdentifier);
+ writer.WriteLine(UserSuppliedIdentifier);
writer.WriteLine(ProviderEndpoint);
writer.WriteLine(Protocol.Version);
+ // No reason to serialize priority. We only needed priority to decide whether to use this endpoint.
}
/// <summary>
@@ -115,16 +233,19 @@ namespace DotNetOpenId.RelyingParty {
internal static ServiceEndpoint Deserialize(TextReader reader) {
var claimedIdentifier = Identifier.Parse(reader.ReadLine());
var providerLocalIdentifier = Identifier.Parse(reader.ReadLine());
+ string userSuppliedIdentifier = reader.ReadLine();
+ if (userSuppliedIdentifier.Length == 0) userSuppliedIdentifier = null;
var providerEndpoint = new Uri(reader.ReadLine());
var protocol = Util.FindBestVersion(p => p.Version, new[] { new Version(reader.ReadLine()) });
- return new ServiceEndpoint(claimedIdentifier, providerEndpoint,
- providerLocalIdentifier, protocol);
+ return new ServiceEndpoint(claimedIdentifier, userSuppliedIdentifier,
+ providerEndpoint, providerLocalIdentifier, protocol);
}
- internal static ServiceEndpoint ParseFromAuthResponse(IDictionary<string, string> query) {
+ internal static ServiceEndpoint ParseFromAuthResponse(IDictionary<string, string> query, Identifier userSuppliedIdentifier) {
Protocol protocol = Protocol.Detect(query);
Debug.Assert(protocol.openid.op_endpoint != null, "This method should only be called in OpenID 2.0 contexts.");
return new ServiceEndpoint(
Util.GetRequiredArg(query, protocol.openid.claimed_id),
+ userSuppliedIdentifier,
new Uri(Util.GetRequiredArg(query, protocol.openid.op_endpoint)),
Util.GetRequiredArg(query, protocol.openid.identity),
protocol);
@@ -142,6 +263,7 @@ namespace DotNetOpenId.RelyingParty {
ServiceEndpoint other = obj as ServiceEndpoint;
if (other == null) return false;
// We specifically do not check our ProviderSupportedServiceTypeUris array
+ // or the priority field
// as that is not persisted in our tokens, and it is not part of the
// important assertion validation that is part of the spec.
return
@@ -154,7 +276,47 @@ namespace DotNetOpenId.RelyingParty {
return ClaimedIdentifier.GetHashCode();
}
public override string ToString() {
- return ProviderEndpoint.AbsoluteUri;
+ StringBuilder builder = new StringBuilder();
+ builder.AppendLine("ClaimedIdentifier: " + ClaimedIdentifier);
+ builder.AppendLine("ProviderLocalIdentifier: " + ProviderLocalIdentifier);
+ builder.AppendLine("ProviderEndpoint: " + ProviderEndpoint.AbsoluteUri);
+ builder.AppendLine("OpenID version: " + Protocol.Version);
+ builder.AppendLine("Service Type URIs:");
+ if (ProviderSupportedServiceTypeUris != null) {
+ foreach (string serviceTypeUri in ProviderSupportedServiceTypeUris) {
+ builder.Append("\t");
+ var matchingExtension = Util.FirstOrDefault(ExtensionManager.RequestExtensions, ext => ext.Key.TypeUri == serviceTypeUri);
+ if (matchingExtension.Key != null) {
+ builder.AppendLine(string.Format(CultureInfo.CurrentCulture, "{0} ({1})", serviceTypeUri, matchingExtension.Value));
+ } else {
+ builder.AppendLine(serviceTypeUri);
+ }
+ }
+ } else {
+ builder.AppendLine("\t(unavailable)");
+ }
+ builder.Length -= Environment.NewLine.Length; // trim last newline
+ return builder.ToString();
}
+
+ #region IXrdsProviderEndpoint Members
+
+ private int? servicePriority;
+ /// <summary>
+ /// Gets the priority associated with this service that may have been given
+ /// in the XRDS document.
+ /// </summary>
+ int? IXrdsProviderEndpoint.ServicePriority {
+ get { return servicePriority; }
+ }
+ private int? uriPriority;
+ /// <summary>
+ /// Gets the priority associated with the service endpoint URL.
+ /// </summary>
+ int? IXrdsProviderEndpoint.UriPriority {
+ get { return uriPriority; }
+ }
+
+ #endregion
}
} \ No newline at end of file
diff --git a/src/DotNetOpenId/RelyingParty/Token.cs b/src/DotNetOpenId/RelyingParty/Token.cs
index 29d442c..68a4e76 100644
--- a/src/DotNetOpenId/RelyingParty/Token.cs
+++ b/src/DotNetOpenId/RelyingParty/Token.cs
@@ -37,18 +37,18 @@ namespace DotNetOpenId.RelyingParty {
/// This string is cryptographically signed to protect against tampering.
/// </summary>
public string Serialize(INonceStore store) {
- using (MemoryStream ms = new MemoryStream()) {
+ using (MemoryStream dataStream = new MemoryStream()) {
if (!persistSignature(store)) {
Debug.Assert(!persistNonce(Endpoint, store), "Without a signature, a nonce is meaningless.");
- StreamWriter writer = new StreamWriter(ms);
+ dataStream.WriteByte(0); // there will be NO signature.
+ StreamWriter writer = new StreamWriter(dataStream);
Endpoint.Serialize(writer);
writer.Flush();
- return Convert.ToBase64String(ms.ToArray());
+ return Convert.ToBase64String(dataStream.ToArray());
} else {
using (HashAlgorithm shaHash = createHashAlgorithm(store))
- using (CryptoStream shaStream = new CryptoStream(ms, shaHash, CryptoStreamMode.Write)) {
+ using (CryptoStream shaStream = new CryptoStream(dataStream, shaHash, CryptoStreamMode.Write)) {
StreamWriter writer = new StreamWriter(shaStream);
-
Endpoint.Serialize(writer);
if (persistNonce(Endpoint, store))
writer.WriteLine(Nonce.Code);
@@ -58,9 +58,10 @@ namespace DotNetOpenId.RelyingParty {
shaStream.FlushFinalBlock();
byte[] hash = shaHash.Hash;
- byte[] data = new byte[hash.Length + ms.Length];
- Buffer.BlockCopy(hash, 0, data, 0, hash.Length);
- Buffer.BlockCopy(ms.ToArray(), 0, data, hash.Length, (int)ms.Length);
+ byte[] data = new byte[1 + hash.Length + dataStream.Length];
+ data[0] = 1; // there is a signature
+ Buffer.BlockCopy(hash, 0, data, 1, hash.Length);
+ Buffer.BlockCopy(dataStream.ToArray(), 0, data, 1 + hash.Length, (int)dataStream.Length);
return Convert.ToBase64String(data);
}
@@ -78,35 +79,49 @@ namespace DotNetOpenId.RelyingParty {
/// by discovery (slow but secure).
/// </remarks>
public static Token Deserialize(string token, INonceStore store) {
- byte[] tok = Convert.FromBase64String(token);
- MemoryStream ms;
+ byte[] tok;
+ try {
+ tok = Convert.FromBase64String(token);
+ } catch (FormatException ex) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ExpectedBase64OpenIdQueryParameter, token), null, ex);
+ }
+ if (tok.Length < 1) throw new OpenIdException(Strings.InvalidSignature);
+ bool signaturePresent = tok[0] == 1;
+ bool signatureVerified = false;
+ MemoryStream dataStream;
- if (persistSignature(store)) {
- // Verify the signature to guarantee that our state hasn't been
- // tampered with in transit or on the provider.
- HashAlgorithm hmac = createHashAlgorithm(store);
- byte[] sig = new byte[hmac.HashSize / 8];
- if (tok.Length < sig.Length)
- throw new OpenIdException(Strings.InvalidSignature);
- Buffer.BlockCopy(tok, 0, sig, 0, sig.Length);
- ms = new MemoryStream(tok, sig.Length, tok.Length - sig.Length);
- byte[] newSig = hmac.ComputeHash(ms);
- ms.Seek(0, SeekOrigin.Begin);
- for (int i = 0; i < sig.Length; i++)
- if (sig[i] != newSig[i])
+ if (signaturePresent) {
+ if (persistSignature(store)) {
+ // Verify the signature to guarantee that our state hasn't been
+ // tampered with in transit or on the provider.
+ HashAlgorithm hmac = createHashAlgorithm(store);
+ int signatureLength = hmac.HashSize / 8;
+ dataStream = new MemoryStream(tok, 1 + signatureLength, tok.Length - 1 - signatureLength);
+ byte[] newSig = hmac.ComputeHash(dataStream);
+ dataStream.Position = 0;
+ if (tok.Length - 1 < newSig.Length)
throw new OpenIdException(Strings.InvalidSignature);
+ for (int i = 0; i < newSig.Length; i++)
+ if (tok[i + 1] != newSig[i])
+ throw new OpenIdException(Strings.InvalidSignature);
+ signatureVerified = true;
+ } else {
+ // Oops, we have no application state, so we have no way of validating the signature.
+ throw new OpenIdException(Strings.InconsistentAppState);
+ }
} else {
- ms = new MemoryStream(tok);
+ dataStream = new MemoryStream(tok, 1, tok.Length - 1);
}
- StreamReader reader = new StreamReader(ms);
+ StreamReader reader = new StreamReader(dataStream);
ServiceEndpoint endpoint = ServiceEndpoint.Deserialize(reader);
Nonce nonce = null;
- if (persistNonce(endpoint, store)) {
+ if (signatureVerified && persistNonce(endpoint, store)) {
nonce = new Nonce(reader.ReadLine(), false);
nonce.Consume(store);
}
- if (!persistSignature(store)) {
+ if (!signatureVerified) {
verifyEndpointByDiscovery(endpoint);
}
@@ -152,7 +167,16 @@ namespace DotNetOpenId.RelyingParty {
/// verification, this is the only alternative.
/// </remarks>
static void verifyEndpointByDiscovery(ServiceEndpoint endpoint) {
- if (!endpoint.Equals(endpoint.ClaimedIdentifier.Discover())) {
+ // If the user entered an OP Identifier then the ClaimedIdentifier will be the special
+ // identifier that we can't perform discovery on. We need to be careful about that.
+ Identifier identifierToDiscover;
+ if (endpoint.ClaimedIdentifier == endpoint.Protocol.ClaimedIdentifierForOPIdentifier) {
+ identifierToDiscover = endpoint.UserSuppliedIdentifier;
+ } else {
+ identifierToDiscover = endpoint.ClaimedIdentifier;
+ }
+ var discoveredEndpoints = new List<ServiceEndpoint>(identifierToDiscover.Discover());
+ if (!discoveredEndpoints.Contains(endpoint)) {
throw new OpenIdException(Strings.InvalidSignature);
}
}
diff --git a/src/DotNetOpenId/RelyingParty/login_failure.png b/src/DotNetOpenId/RelyingParty/login_failure.png
new file mode 100644
index 0000000..8003700
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/login_failure.png
Binary files differ
diff --git a/src/DotNetOpenId/RelyingParty/login_success (lock).png b/src/DotNetOpenId/RelyingParty/login_success (lock).png
new file mode 100644
index 0000000..bc0c0c8
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/login_success (lock).png
Binary files differ
diff --git a/src/DotNetOpenId/RelyingParty/login_success.png b/src/DotNetOpenId/RelyingParty/login_success.png
new file mode 100644
index 0000000..0ae1365
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/login_success.png
Binary files differ
diff --git a/src/DotNetOpenId/RelyingParty/spinner.gif b/src/DotNetOpenId/RelyingParty/spinner.gif
new file mode 100644
index 0000000..9cb298e
--- /dev/null
+++ b/src/DotNetOpenId/RelyingParty/spinner.gif
Binary files differ