summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenId
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenId')
-rw-r--r--src/DotNetOpenId/.gitignore1
-rw-r--r--src/DotNetOpenId/Association.cs10
-rw-r--r--src/DotNetOpenId/Configuration/ProviderSection.cs27
-rw-r--r--src/DotNetOpenId/Configuration/ProviderSecuritySettingsElement.cs38
-rw-r--r--src/DotNetOpenId/Configuration/RelyingPartySection.cs27
-rw-r--r--src/DotNetOpenId/Configuration/RelyingPartySecuritySettingsElement.cs45
-rw-r--r--src/DotNetOpenId/Configuration/StoreElement.cs24
-rw-r--r--src/DotNetOpenId/Configuration/UntrustedWebRequestSection.cs78
-rw-r--r--src/DotNetOpenId/Configuration/WhiteBlackListCollection.cs33
-rw-r--r--src/DotNetOpenId/Configuration/WhiteBlackListElement.cs13
-rw-r--r--src/DotNetOpenId/DiffieHellmanUtil.cs (renamed from src/DotNetOpenId/CryptUtil.cs)57
-rw-r--r--src/DotNetOpenId/DotNetOpenId.csproj77
-rw-r--r--src/DotNetOpenId/ExtensionArgumentsManager.cs5
-rw-r--r--src/DotNetOpenId/Extensions/AliasManager.cs60
-rw-r--r--src/DotNetOpenId/Extensions/AttributeExchange/AttributeValues.cs13
-rw-r--r--src/DotNetOpenId/Extensions/AttributeExchange/FetchRequest.cs25
-rw-r--r--src/DotNetOpenId/Extensions/AttributeExchange/FetchResponse.cs19
-rw-r--r--src/DotNetOpenId/Extensions/AttributeExchange/StoreRequest.cs12
-rw-r--r--src/DotNetOpenId/Extensions/AttributeExchange/StoreResponse.cs5
-rw-r--r--src/DotNetOpenId/Extensions/ExtensionManager.cs23
-rw-r--r--src/DotNetOpenId/Extensions/IClientScriptExtensionResponse.cs27
-rw-r--r--src/DotNetOpenId/Extensions/IExtension.cs36
-rw-r--r--src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs7
-rw-r--r--src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs32
-rw-r--r--src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs2
-rw-r--r--src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs93
-rw-r--r--src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs92
-rw-r--r--src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsRequest.cs26
-rw-r--r--src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs77
-rw-r--r--src/DotNetOpenId/Extensions/SimpleRegistration/Constants.cs3
-rw-r--r--src/DotNetOpenId/GlobalSuppressions.cs1
-rw-r--r--src/DotNetOpenId/HmacSha1Association.cs22
-rw-r--r--src/DotNetOpenId/HmacSha256Association.cs22
-rw-r--r--src/DotNetOpenId/HmacShaAssociation.cs145
-rw-r--r--src/DotNetOpenId/IEncodable.cs6
-rw-r--r--src/DotNetOpenId/IResponse.cs8
-rw-r--r--src/DotNetOpenId/Identifier.cs63
-rw-r--r--src/DotNetOpenId/KeyValueFormEncoding.cs6
-rw-r--r--src/DotNetOpenId/Logger.cs199
-rw-r--r--src/DotNetOpenId/Loggers/ILog.cs964
-rw-r--r--src/DotNetOpenId/Loggers/Log4NetLogger.cs207
-rw-r--r--src/DotNetOpenId/Loggers/NoOpLogger.cs177
-rw-r--r--src/DotNetOpenId/Loggers/TraceLogger.cs201
-rw-r--r--src/DotNetOpenId/MessageEncoder.cs235
-rw-r--r--src/DotNetOpenId/NoDiscoveryIdentifier.cs43
-rw-r--r--src/DotNetOpenId/Nonce.cs3
-rw-r--r--src/DotNetOpenId/OpenIdException.cs61
-rw-r--r--src/DotNetOpenId/Properties/AssemblyInfo.cs49
-rw-r--r--src/DotNetOpenId/Protocol.cs68
-rw-r--r--src/DotNetOpenId/Provider/AssertionMessage.cs21
-rw-r--r--src/DotNetOpenId/Provider/AssociateRequest.cs7
-rw-r--r--src/DotNetOpenId/Provider/CheckAuthRequest.cs8
-rw-r--r--src/DotNetOpenId/Provider/CheckIdRequest.cs89
-rw-r--r--src/DotNetOpenId/Provider/FaultyRequest.cs2
-rw-r--r--src/DotNetOpenId/Provider/IAuthenticationRequest.cs43
-rw-r--r--src/DotNetOpenId/Provider/IdentityEndpoint.cs144
-rw-r--r--src/DotNetOpenId/Provider/OpenIdProvider.cs43
-rw-r--r--src/DotNetOpenId/Provider/ProviderEndpoint.cs21
-rw-r--r--src/DotNetOpenId/Provider/ProviderSecuritySettings.cs26
-rw-r--r--src/DotNetOpenId/Provider/ProviderSession.cs10
-rw-r--r--src/DotNetOpenId/Provider/Request.cs42
-rw-r--r--src/DotNetOpenId/Provider/Signatory.cs40
-rw-r--r--src/DotNetOpenId/Provider/SigningMessageEncoder.cs12
-rw-r--r--src/DotNetOpenId/Realm.cs16
-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
-rw-r--r--src/DotNetOpenId/Response.cs3
-rw-r--r--src/DotNetOpenId/SecuritySettings.cs58
-rw-r--r--src/DotNetOpenId/Strings.Designer.cs210
-rw-r--r--src/DotNetOpenId/Strings.resx82
-rw-r--r--src/DotNetOpenId/TraceUtil.cs45
-rw-r--r--src/DotNetOpenId/UntrustedWebRequest.cs106
-rw-r--r--src/DotNetOpenId/UntrustedWebResponse.cs53
-rw-r--r--src/DotNetOpenId/UriIdentifier.cs236
-rw-r--r--src/DotNetOpenId/Util.cs316
-rw-r--r--src/DotNetOpenId/XrdsPublisher.cs4
-rw-r--r--src/DotNetOpenId/XriIdentifier.cs76
-rw-r--r--src/DotNetOpenId/Yadis/ContentTypes.cs1
-rw-r--r--src/DotNetOpenId/Yadis/ServiceElement.cs33
-rw-r--r--src/DotNetOpenId/Yadis/UriElement.cs33
-rw-r--r--src/DotNetOpenId/Yadis/XrdElement.cs29
-rw-r--r--src/DotNetOpenId/Yadis/XrdsDocument.cs104
-rw-r--r--src/DotNetOpenId/Yadis/XrdsNode.cs11
-rw-r--r--src/DotNetOpenId/Yadis/Yadis.cs87
114 files changed, 7681 insertions, 1021 deletions
diff --git a/src/DotNetOpenId/.gitignore b/src/DotNetOpenId/.gitignore
index 5c901c6..2a08796 100644
--- a/src/DotNetOpenId/.gitignore
+++ b/src/DotNetOpenId/.gitignore
@@ -2,3 +2,4 @@
bin
obj
Bin
+StyleCop.Cache
diff --git a/src/DotNetOpenId/Association.cs b/src/DotNetOpenId/Association.cs
index 5c9c302..ee3fd70 100644
--- a/src/DotNetOpenId/Association.cs
+++ b/src/DotNetOpenId/Association.cs
@@ -56,11 +56,11 @@ namespace DotNetOpenId {
TimeSpan remainingLifeLength = expires - DateTime.UtcNow;
byte[] secret = privateData; // the whole of privateData is the secret key for now.
// We figure out what derived type to instantiate based on the length of the secret.
- if(secret.Length == CryptUtil.Sha1.HashSize / 8)
- return new HmacSha1Association(handle, secret, remainingLifeLength);
- if (secret.Length == CryptUtil.Sha256.HashSize / 8)
- return new HmacSha256Association(handle, secret, remainingLifeLength);
- throw new ArgumentException(Strings.BadAssociationPrivateData, "privateData");
+ try {
+ return HmacShaAssociation.Create(secret.Length, handle, secret, remainingLifeLength);
+ } catch (ArgumentException ex) {
+ throw new ArgumentException(Strings.BadAssociationPrivateData, "privateData", ex);
+ }
}
static TimeSpan minimumUsefulAssociationLifetime {
diff --git a/src/DotNetOpenId/Configuration/ProviderSection.cs b/src/DotNetOpenId/Configuration/ProviderSection.cs
new file mode 100644
index 0000000..cfd6052
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/ProviderSection.cs
@@ -0,0 +1,27 @@
+using System.Configuration;
+using DotNetOpenId.Provider;
+using IProviderAssociationStore = DotNetOpenId.IAssociationStore<DotNetOpenId.AssociationRelyingPartyType>;
+
+namespace DotNetOpenId.Configuration {
+ internal class ProviderSection : ConfigurationSection {
+ internal static ProviderSection Configuration {
+ get { return (ProviderSection)ConfigurationManager.GetSection("dotNetOpenId/provider") ?? new ProviderSection(); }
+ }
+
+ public ProviderSection() { }
+
+ const string securitySettingsConfigName = "security";
+ [ConfigurationProperty(securitySettingsConfigName)]
+ public ProviderSecuritySettingsElement SecuritySettings {
+ get { return (ProviderSecuritySettingsElement)this[securitySettingsConfigName] ?? new ProviderSecuritySettingsElement(); }
+ set { this[securitySettingsConfigName] = value; }
+ }
+
+ const string storeConfigName = "store";
+ [ConfigurationProperty(storeConfigName)]
+ public StoreConfigurationElement<IProviderAssociationStore> Store {
+ get { return (StoreConfigurationElement<IProviderAssociationStore>)this[storeConfigName] ?? new StoreConfigurationElement<IProviderAssociationStore>(); }
+ set { this[storeConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/ProviderSecuritySettingsElement.cs b/src/DotNetOpenId/Configuration/ProviderSecuritySettingsElement.cs
new file mode 100644
index 0000000..8f3ffb8
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/ProviderSecuritySettingsElement.cs
@@ -0,0 +1,38 @@
+using System.Configuration;
+using DotNetOpenId.Provider;
+
+namespace DotNetOpenId.Configuration {
+ internal class ProviderSecuritySettingsElement : ConfigurationElement {
+ public ProviderSecuritySettingsElement() {
+ }
+
+ public ProviderSecuritySettings CreateSecuritySettings() {
+ ProviderSecuritySettings settings = new ProviderSecuritySettings();
+ settings.MinimumHashBitLength = MinimumHashBitLength;
+ settings.MaximumHashBitLength = MaximumHashBitLength;
+ settings.ProtectDownlevelReplayAttacks = ProtectDownlevelReplayAttacks;
+ return settings;
+ }
+
+ const string minimumHashBitLengthConfigName = "minimumHashBitLength";
+ [ConfigurationProperty(minimumHashBitLengthConfigName, DefaultValue = DotNetOpenId.SecuritySettings.minimumHashBitLengthDefault)]
+ public int MinimumHashBitLength {
+ get { return (int)this[minimumHashBitLengthConfigName]; }
+ set { this[minimumHashBitLengthConfigName] = value; }
+ }
+
+ const string maximumHashBitLengthConfigName = "maximumHashBitLength";
+ [ConfigurationProperty(maximumHashBitLengthConfigName, DefaultValue = DotNetOpenId.SecuritySettings.maximumHashBitLengthRPDefault)]
+ public int MaximumHashBitLength {
+ get { return (int)this[maximumHashBitLengthConfigName]; }
+ set { this[maximumHashBitLengthConfigName] = value; }
+ }
+
+ const string protectDownlevelReplayAttacksConfigName = "protectDownlevelReplayAttacks";
+ [ConfigurationProperty(protectDownlevelReplayAttacksConfigName, DefaultValue = false)]
+ public bool ProtectDownlevelReplayAttacks {
+ get { return (bool)this[protectDownlevelReplayAttacksConfigName]; }
+ set { this[protectDownlevelReplayAttacksConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/RelyingPartySection.cs b/src/DotNetOpenId/Configuration/RelyingPartySection.cs
new file mode 100644
index 0000000..100641c
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/RelyingPartySection.cs
@@ -0,0 +1,27 @@
+using System.Configuration;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId.Configuration {
+ internal class RelyingPartySection : ConfigurationSection {
+ internal static RelyingPartySection Configuration {
+ get { return (RelyingPartySection)ConfigurationManager.GetSection("dotNetOpenId/relyingParty") ?? new RelyingPartySection(); }
+ }
+
+ public RelyingPartySection() {
+ }
+
+ const string securitySettingsConfigName = "security";
+ [ConfigurationProperty(securitySettingsConfigName)]
+ public RelyingPartySecuritySettingsElement SecuritySettings {
+ get { return (RelyingPartySecuritySettingsElement)this[securitySettingsConfigName] ?? new RelyingPartySecuritySettingsElement(); }
+ set { this[securitySettingsConfigName] = value; }
+ }
+
+ const string storeConfigName = "store";
+ [ConfigurationProperty(storeConfigName)]
+ public StoreConfigurationElement<IRelyingPartyApplicationStore> Store {
+ get { return (StoreConfigurationElement<IRelyingPartyApplicationStore>)this[storeConfigName] ?? new StoreConfigurationElement<IRelyingPartyApplicationStore>(); }
+ set { this[storeConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/RelyingPartySecuritySettingsElement.cs b/src/DotNetOpenId/Configuration/RelyingPartySecuritySettingsElement.cs
new file mode 100644
index 0000000..a76d993
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/RelyingPartySecuritySettingsElement.cs
@@ -0,0 +1,45 @@
+using System.Configuration;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId.Configuration {
+ internal class RelyingPartySecuritySettingsElement : ConfigurationElement {
+ public RelyingPartySecuritySettingsElement() { }
+
+ public RelyingPartySecuritySettings CreateSecuritySettings() {
+ RelyingPartySecuritySettings settings = new RelyingPartySecuritySettings();
+ settings.RequireSsl = RequireSsl;
+ settings.MinimumRequiredOpenIdVersion = MinimumRequiredOpenIdVersion;
+ settings.MinimumHashBitLength = MinimumHashBitLength;
+ settings.MaximumHashBitLength = MaximumHashBitLength;
+ return settings;
+ }
+
+ const string requireSslConfigName = "requireSsl";
+ [ConfigurationProperty(requireSslConfigName, DefaultValue = false)]
+ public bool RequireSsl {
+ get { return (bool)this[requireSslConfigName]; }
+ set { this[requireSslConfigName] = value; }
+ }
+
+ const string minimumRequiredOpenIdVersionConfigName = "minimumRequiredOpenIdVersion";
+ [ConfigurationProperty(minimumRequiredOpenIdVersionConfigName, DefaultValue = "V10")]
+ public ProtocolVersion MinimumRequiredOpenIdVersion {
+ get { return (ProtocolVersion)this[minimumRequiredOpenIdVersionConfigName]; }
+ set { this[minimumRequiredOpenIdVersionConfigName] = value; }
+ }
+
+ const string minimumHashBitLengthConfigName = "minimumHashBitLength";
+ [ConfigurationProperty(minimumHashBitLengthConfigName, DefaultValue = DotNetOpenId.SecuritySettings.minimumHashBitLengthDefault)]
+ public int MinimumHashBitLength {
+ get { return (int)this[minimumHashBitLengthConfigName]; }
+ set { this[minimumHashBitLengthConfigName] = value; }
+ }
+
+ const string maximumHashBitLengthConfigName = "maximumHashBitLength";
+ [ConfigurationProperty(maximumHashBitLengthConfigName, DefaultValue = DotNetOpenId.SecuritySettings.maximumHashBitLengthRPDefault)]
+ public int MaximumHashBitLength {
+ get { return (int)this[maximumHashBitLengthConfigName]; }
+ set { this[maximumHashBitLengthConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/StoreElement.cs b/src/DotNetOpenId/Configuration/StoreElement.cs
new file mode 100644
index 0000000..d2c94cb
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/StoreElement.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Configuration;
+
+namespace DotNetOpenId.Configuration {
+ internal class StoreConfigurationElement<T> : ConfigurationElement {
+ public StoreConfigurationElement() { }
+
+ const string customStoreTypeConfigName = "type";
+ [ConfigurationProperty(customStoreTypeConfigName)]
+ //[SubclassTypeValidator(typeof(T))]
+ public string TypeName {
+ get { return (string)this[customStoreTypeConfigName]; }
+ set { this[customStoreTypeConfigName] = value; }
+ }
+
+ public Type CustomStoreType {
+ get { return string.IsNullOrEmpty(TypeName) ? null : Type.GetType(TypeName); }
+ }
+
+ public T CreateInstanceOfStore(T defaultValue) {
+ return CustomStoreType != null ? (T)Activator.CreateInstance(CustomStoreType) : defaultValue;
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/UntrustedWebRequestSection.cs b/src/DotNetOpenId/Configuration/UntrustedWebRequestSection.cs
new file mode 100644
index 0000000..392acf8
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/UntrustedWebRequestSection.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Configuration;
+
+namespace DotNetOpenId.Configuration {
+ internal class UntrustedWebRequestSection : ConfigurationSection {
+ internal static UntrustedWebRequestSection Configuration {
+ get { return (UntrustedWebRequestSection)ConfigurationManager.GetSection("dotNetOpenId/untrustedWebRequest") ?? new UntrustedWebRequestSection(); }
+ }
+
+ public UntrustedWebRequestSection() {
+ SectionInformation.AllowLocation = false;
+ }
+
+ const string readWriteTimeoutConfigName = "readWriteTimeout";
+ [ConfigurationProperty(readWriteTimeoutConfigName, DefaultValue = "00:00:00.800")]
+ [PositiveTimeSpanValidator]
+ public TimeSpan ReadWriteTimeout {
+ get { return (TimeSpan)this[readWriteTimeoutConfigName]; }
+ set { this[readWriteTimeoutConfigName] = value; }
+ }
+
+ const string timeoutConfigName = "timeout";
+ [ConfigurationProperty(timeoutConfigName, DefaultValue = "00:00:10")]
+ [PositiveTimeSpanValidator]
+ public TimeSpan Timeout {
+ get { return (TimeSpan)this[timeoutConfigName]; }
+ set { this[timeoutConfigName] = value; }
+ }
+
+ const string maximumBytesToReadConfigName = "maximumBytesToRead";
+ [ConfigurationProperty(maximumBytesToReadConfigName, DefaultValue = 1024 * 1024)]
+ [IntegerValidator(MinValue = 2048)]
+ public int MaximumBytesToRead {
+ get { return (int)this[maximumBytesToReadConfigName]; }
+ set { this[maximumBytesToReadConfigName] = value; }
+ }
+
+ const string maximumRedirectionsConfigName = "maximumRedirections";
+ [ConfigurationProperty(maximumRedirectionsConfigName, DefaultValue = 10)]
+ [IntegerValidator(MinValue = 0)]
+ public int MaximumRedirections {
+ get { return (int)this[maximumRedirectionsConfigName]; }
+ set { this[maximumRedirectionsConfigName] = value; }
+ }
+
+ const string whitelistHostsConfigName = "whitelistHosts";
+ [ConfigurationProperty(whitelistHostsConfigName, IsDefaultCollection = false)]
+ [ConfigurationCollection(typeof(WhiteBlackListCollection))]
+ public WhiteBlackListCollection WhitelistHosts {
+ get { return (WhiteBlackListCollection)this[whitelistHostsConfigName] ?? new WhiteBlackListCollection(); }
+ set { this[whitelistHostsConfigName] = value; }
+ }
+
+ const string blacklistHostsConfigName = "blacklistHosts";
+ [ConfigurationProperty(blacklistHostsConfigName, IsDefaultCollection = false)]
+ [ConfigurationCollection(typeof(WhiteBlackListCollection))]
+ public WhiteBlackListCollection BlacklistHosts {
+ get { return (WhiteBlackListCollection)this[blacklistHostsConfigName] ?? new WhiteBlackListCollection(); }
+ set { this[blacklistHostsConfigName] = value; }
+ }
+
+ const string whitelistHostsRegexConfigName = "whitelistHostsRegex";
+ [ConfigurationProperty(whitelistHostsRegexConfigName, IsDefaultCollection = false)]
+ [ConfigurationCollection(typeof(WhiteBlackListCollection))]
+ public WhiteBlackListCollection WhitelistHostsRegex {
+ get { return (WhiteBlackListCollection)this[whitelistHostsRegexConfigName] ?? new WhiteBlackListCollection(); }
+ set { this[whitelistHostsRegexConfigName] = value; }
+ }
+
+ const string blacklistHostsRegexConfigName = "blacklistHostsRegex";
+ [ConfigurationProperty(blacklistHostsRegexConfigName, IsDefaultCollection = false)]
+ [ConfigurationCollection(typeof(WhiteBlackListCollection))]
+ public WhiteBlackListCollection BlacklistHostsRegex {
+ get { return (WhiteBlackListCollection)this[blacklistHostsRegexConfigName] ?? new WhiteBlackListCollection(); }
+ set { this[blacklistHostsRegexConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/WhiteBlackListCollection.cs b/src/DotNetOpenId/Configuration/WhiteBlackListCollection.cs
new file mode 100644
index 0000000..29485d1
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/WhiteBlackListCollection.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Configuration;
+using System.Text.RegularExpressions;
+
+namespace DotNetOpenId.Configuration {
+ internal class WhiteBlackListCollection : ConfigurationElementCollection {
+ public WhiteBlackListCollection() { }
+
+ protected override ConfigurationElement CreateNewElement() {
+ return new WhiteBlackListElement();
+ }
+
+ protected override object GetElementKey(ConfigurationElement element) {
+ return ((WhiteBlackListElement)element).Name;
+ }
+
+ internal IEnumerable<string> KeysAsStrings {
+ get {
+ foreach (WhiteBlackListElement element in this) {
+ yield return element.Name;
+ }
+ }
+ }
+
+ internal IEnumerable<Regex> KeysAsRegexs {
+ get {
+ foreach (WhiteBlackListElement element in this) {
+ yield return new Regex(element.Name);
+ }
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Configuration/WhiteBlackListElement.cs b/src/DotNetOpenId/Configuration/WhiteBlackListElement.cs
new file mode 100644
index 0000000..02a909d
--- /dev/null
+++ b/src/DotNetOpenId/Configuration/WhiteBlackListElement.cs
@@ -0,0 +1,13 @@
+using System.Configuration;
+
+namespace DotNetOpenId.Configuration {
+ internal class WhiteBlackListElement : ConfigurationElement {
+ const string nameConfigName = "name";
+ [ConfigurationProperty(nameConfigName, IsRequired = true)]
+ //[StringValidator(MinLength = 1)]
+ public string Name {
+ get { return (string)this[nameConfigName]; }
+ set { this[nameConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenId/CryptUtil.cs b/src/DotNetOpenId/DiffieHellmanUtil.cs
index 254b4cb..ac3bcff 100644
--- a/src/DotNetOpenId/CryptUtil.cs
+++ b/src/DotNetOpenId/DiffieHellmanUtil.cs
@@ -1,13 +1,47 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
+using System;
+using System.Globalization;
using System.Security.Cryptography;
using Org.Mentalis.Security.Cryptography;
-using System.Globalization;
-
namespace DotNetOpenId {
- internal static class CryptUtil {
+ class DiffieHellmanUtil {
+ class DHSha {
+ public DHSha(HashAlgorithm algorithm, Util.Func<Protocol, string> getName) {
+ if (algorithm == null) throw new ArgumentNullException("algorithm");
+ if (getName == null) throw new ArgumentNullException("getName");
+
+ GetName = getName;
+ Algorithm = algorithm;
+ }
+ internal Util.Func<Protocol, string> GetName;
+ internal readonly HashAlgorithm Algorithm;
+ }
+
+ static DHSha[] DiffieHellmanSessionTypes = {
+ new DHSha(new SHA512Managed(), protocol => protocol.Args.SessionType.DH_SHA512),
+ new DHSha(new SHA384Managed(), protocol => protocol.Args.SessionType.DH_SHA384),
+ new DHSha(new SHA256Managed(), protocol => protocol.Args.SessionType.DH_SHA256),
+ new DHSha(new SHA1Managed(), protocol => protocol.Args.SessionType.DH_SHA1),
+ };
+
+ public static HashAlgorithm Lookup(Protocol protocol, string name) {
+ foreach (DHSha dhsha in DiffieHellmanSessionTypes) {
+ if (String.Equals(dhsha.GetName(protocol), name, StringComparison.Ordinal)) {
+ return dhsha.Algorithm;
+ }
+ }
+ throw new ArgumentOutOfRangeException("name");
+ }
+
+ public static string GetNameForSize(Protocol protocol, int hashSizeInBits) {
+ foreach (DHSha dhsha in DiffieHellmanSessionTypes) {
+ if (dhsha.Algorithm.HashSize == hashSizeInBits) {
+ return dhsha.GetName(protocol);
+ }
+ }
+ return null;
+ }
+
public static byte[] DEFAULT_GEN = { 2 };
public static byte[] DEFAULT_MOD = {0, 220, 249, 58, 11, 136, 57, 114, 236, 14, 25, 152, 154, 197, 162,
206, 49, 14, 29, 55, 113, 126, 141, 149, 113, 187, 118, 35, 115, 24,
@@ -19,13 +53,6 @@ namespace DotNetOpenId {
154, 72, 59, 138, 118, 34, 62, 93, 73, 10, 37, 127, 5, 189, 255, 22,
242, 251, 34, 197, 131, 171};
- internal static SHA1CryptoServiceProvider Sha1 = new SHA1CryptoServiceProvider();
- internal static SHA256Managed Sha256 = new SHA256Managed();
-
- public static string UnsignedToBase64(byte[] inputBytes) {
- return Convert.ToBase64String(ensurePositive(inputBytes));
- }
-
public static DiffieHellman CreateDiffieHellman() {
return new DiffieHellmanManaged(DEFAULT_MOD, DEFAULT_GEN, 1024);
}
@@ -46,6 +73,10 @@ namespace DotNetOpenId {
return secret;
}
+ public static string UnsignedToBase64(byte[] inputBytes) {
+ return Convert.ToBase64String(ensurePositive(inputBytes));
+ }
+
/// <summary>
/// Ensures that the big integer represented by a given series of bytes
/// is a positive integer.
diff --git a/src/DotNetOpenId/DotNetOpenId.csproj b/src/DotNetOpenId/DotNetOpenId.csproj
index a908853..d68b245 100644
--- a/src/DotNetOpenId/DotNetOpenId.csproj
+++ b/src/DotNetOpenId/DotNetOpenId.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
- <ProductVersion>9.0.21022</ProductVersion>
+ <ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{5D6EDC86-F5B2-4786-8376-4E7C24C63D39}</ProjectGuid>
<OutputType>Library</OutputType>
@@ -16,12 +16,12 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\bin\debug\</OutputPath>
- <DefineConstants>DEBUG;TRACE</DefineConstants>
+ <DefineConstants>TRACE;DEBUG</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<DocumentationFile>..\..\bin\debug\DotNetOpenId.xml</DocumentationFile>
- <RunCodeAnalysis>true</RunCodeAnalysis>
+ <RunCodeAnalysis>false</RunCodeAnalysis>
<CodeAnalysisRules>-Microsoft.Design#CA1054;-Microsoft.Design#CA1056;-Microsoft.Design#CA1055</CodeAnalysisRules>
<NoWarn>
</NoWarn>
@@ -30,13 +30,13 @@
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\bin\Release\</OutputPath>
- <DefineConstants>
- </DefineConstants>
+ <DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<AllowUnsafeBlocks>false</AllowUnsafeBlocks>
<DocumentationFile>..\..\bin\Release\DotNetOpenId.xml</DocumentationFile>
- <RunCodeAnalysis>false</RunCodeAnalysis>
+ <RunCodeAnalysis>true</RunCodeAnalysis>
+ <CodeAnalysisRules>-Microsoft.Design#CA1054;-Microsoft.Design#CA1056;-Microsoft.Design#CA1055</CodeAnalysisRules>
</PropertyGroup>
<PropertyGroup Condition=" '$(Sign)' == 'true' ">
<SignAssembly>true</SignAssembly>
@@ -44,7 +44,12 @@
<DefineConstants>$(DefineConstants);StrongNameSigned</DefineConstants>
</PropertyGroup>
<ItemGroup>
+ <Reference Include="log4net, Version=1.2.10.0, Culture=neutral, PublicKeyToken=1b44e1d426115821, processorArchitecture=MSIL">
+ <SpecificVersion>False</SpecificVersion>
+ <HintPath>..\..\lib\log4net.dll</HintPath>
+ </Reference>
<Reference Include="System" />
+ <Reference Include="System.configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Web" />
@@ -55,7 +60,26 @@
<Compile Include="Association.cs" />
<Compile Include="AssociationMemoryStore.cs" />
<Compile Include="Associations.cs" />
+ <Compile Include="Configuration\UntrustedWebRequestSection.cs" />
+ <Compile Include="Configuration\WhiteBlackListCollection.cs" />
+ <Compile Include="Configuration\WhiteBlackListElement.cs" />
+ <Compile Include="DiffieHellmanUtil.cs" />
+ <Compile Include="Extensions\IClientScriptExtensionResponse.cs" />
+ <Compile Include="Extensions\ExtensionManager.cs" />
+ <Compile Include="Provider\ProviderSecuritySettings.cs" />
+ <Compile Include="RelyingParty\AuthenticationResponseSnapshot.cs" />
+ <Compile Include="RelyingParty\OpenIdAjaxTextBox.cs" />
+ <Compile Include="Configuration\ProviderSecuritySettingsElement.cs" />
+ <Compile Include="Configuration\ProviderSection.cs" />
+ <Compile Include="Configuration\RelyingPartySection.cs" />
+ <Compile Include="RelyingParty\RelyingPartySecuritySettings.cs" />
+ <Compile Include="Configuration\RelyingPartySecuritySettingsElement.cs" />
+ <Compile Include="Configuration\StoreElement.cs" />
+ <Compile Include="SecuritySettings.cs" />
+ <Compile Include="NoDiscoveryIdentifier.cs" />
<Compile Include="Provider\SigningMessageEncoder.cs" />
+ <Compile Include="RelyingParty\DirectMessageHttpChannel.cs" />
+ <Compile Include="RelyingParty\IDirectMessageChannel.cs" />
<Compile Include="RelyingParty\IndirectMessageRequest.cs" />
<Compile Include="ExtensionArgumentsManager.cs" />
<Compile Include="Extensions\AliasManager.cs" />
@@ -74,9 +98,12 @@
<Compile Include="Extensions\AttributeExchange\StoreRequest.cs" />
<Compile Include="Extensions\AttributeExchange\StoreResponse.cs" />
<Compile Include="Extensions\IExtension.cs" />
- <Compile Include="HmacSha256Association.cs" />
<Compile Include="Identifier.cs" />
<Compile Include="IResponse.cs" />
+ <Compile Include="Loggers\TraceLogger.cs" />
+ <Compile Include="Loggers\ILog.cs" />
+ <Compile Include="Loggers\Log4NetLogger.cs" />
+ <Compile Include="Loggers\NoOpLogger.cs" />
<Compile Include="Protocol.cs" />
<Compile Include="Provider\AssertionMessage.cs" />
<Compile Include="Provider\RelyingPartyReceivingEndpoint.cs" />
@@ -87,6 +114,9 @@
<Compile Include="RelyingParty\CheckAuthRequest.cs" />
<Compile Include="RelyingParty\CheckAuthResponse.cs" />
<Compile Include="RelyingParty\ApplicationMemoryStore.cs" />
+ <Compile Include="RelyingParty\IProviderEndpoint.cs" />
+ <Compile Include="RelyingParty\ISetupRequiredAuthenticationResponse.cs" />
+ <Compile Include="RelyingParty\IXrdsProviderEndpoint.cs" />
<Compile Include="RelyingParty\OpenIdMobileTextBox.cs" />
<Compile Include="RelyingParty\DirectRequest.cs" />
<Compile Include="RelyingParty\DirectResponse.cs" />
@@ -101,7 +131,7 @@
<Compile Include="RelyingParty\OpenIdRelyingParty.cs" />
<Compile Include="RelyingParty\Token.cs" />
<Compile Include="GlobalSuppressions.cs" />
- <Compile Include="HmacSha1Association.cs" />
+ <Compile Include="HmacShaAssociation.cs" />
<Compile Include="HttpEncoding.cs" />
<Compile Include="IAssociationStore.cs" />
<Compile Include="KeyValueFormEncoding.cs" />
@@ -126,7 +156,6 @@
<Compile Include="RelyingParty\OpenIdTextBox.cs" />
<Compile Include="Extensions\SimpleRegistration\DemandLevel.cs" />
<Compile Include="RelyingParty\ServiceEndpoint.cs" />
- <Compile Include="CryptUtil.cs" />
<Compile Include="DiffieHellman\DHKeyGeneration.cs" />
<Compile Include="DiffieHellman\DHParameters.cs" />
<Compile Include="DiffieHellman\DiffieHellman.cs" />
@@ -149,7 +178,7 @@
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
- <Compile Include="TraceUtil.cs">
+ <Compile Include="Logger.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Realm.cs" />
@@ -181,12 +210,24 @@
<None Include="RelyingParty\RelyingParty.cd" />
<None Include="Provider\Provider.cd" />
</ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="RelyingParty\OpenIdAjaxTextBox.js" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="..\..\doc\logo\dotnetopenid_16x16.gif">
+ <Link>RelyingParty\dotnetopenid_16x16.gif</Link>
+ </EmbeddedResource>
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="RelyingParty\spinner.gif" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="RelyingParty\login_failure.png" />
+ <EmbeddedResource Include="RelyingParty\login_success %28lock%29.png" />
+ </ItemGroup>
+ <ItemGroup>
+ <EmbeddedResource Include="RelyingParty\login_success.png" />
+ </ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
- <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
- Other similar extension points exist, see Microsoft.Common.targets.
- <Target Name="BeforeBuild">
- </Target>
- <Target Name="AfterBuild">
- </Target>
- -->
-</Project> \ No newline at end of file
+ <Import Project="..\..\tools\DotNetOpenId.Versioning.targets" />
+</Project>
diff --git a/src/DotNetOpenId/ExtensionArgumentsManager.cs b/src/DotNetOpenId/ExtensionArgumentsManager.cs
index 9bcdb73..4f8cdc2 100644
--- a/src/DotNetOpenId/ExtensionArgumentsManager.cs
+++ b/src/DotNetOpenId/ExtensionArgumentsManager.cs
@@ -22,6 +22,7 @@ namespace DotNetOpenId {
/// </summary>
static readonly Dictionary<string, string> typeUriToAliasAffinity = new Dictionary<string, string> {
{ Extensions.SimpleRegistration.Constants.sreg_ns, Extensions.SimpleRegistration.Constants.sreg_compatibility_alias },
+ { Extensions.ProviderAuthenticationPolicy.Constants.TypeUri, Extensions.ProviderAuthenticationPolicy.Constants.pape_compatibility_alias },
};
private ExtensionArgumentsManager() { }
@@ -113,6 +114,10 @@ namespace DotNetOpenId {
}
}
+ /// <summary>
+ /// Gets the fields carried by a given OpenId extension.
+ /// </summary>
+ /// <returns>The fields included in the given extension, or null if the extension is not present.</returns>
public IDictionary<string, string> GetExtensionArguments(string extensionTypeUri) {
if (!isReadMode) throw new InvalidOperationException();
if (string.IsNullOrEmpty(extensionTypeUri)) throw new ArgumentNullException("extensionTypeUri");
diff --git a/src/DotNetOpenId/Extensions/AliasManager.cs b/src/DotNetOpenId/Extensions/AliasManager.cs
index 52308d5..f4bbb04 100644
--- a/src/DotNetOpenId/Extensions/AliasManager.cs
+++ b/src/DotNetOpenId/Extensions/AliasManager.cs
@@ -37,6 +37,61 @@ namespace DotNetOpenId.Extensions {
aliasToTypeUriMap.Add(alias, typeUri);
typeUriToAliasMap.Add(typeUri, alias);
}
+ /// <summary>
+ /// Takes a sequence of type URIs and assigns aliases for all of them.
+ /// </summary>
+ /// <param name="typeUris">The type URIs to create aliases for.</param>
+ /// <param name="preferredTypeUriToAliases">An optional dictionary of URI/alias pairs that suggest preferred aliases to use if available for certain type URIs.</param>
+ public void AssignAliases(IEnumerable<string> typeUris, IDictionary<string, string> preferredTypeUriToAliases) {
+ // First go through the actually used type URIs and see which ones have matching preferred aliases.
+ if (preferredTypeUriToAliases != null) {
+ foreach (string typeUri in typeUris) {
+ if (typeUriToAliasMap.ContainsKey(typeUri)) {
+ // this Type URI is already mapped to an alias.
+ continue;
+ }
+
+ string preferredAlias;
+ if (preferredTypeUriToAliases.TryGetValue(typeUri, out preferredAlias) && !IsAliasUsed(preferredAlias)) {
+ SetAlias(preferredAlias, typeUri);
+ }
+ }
+ }
+
+ // Now go through the whole list again and assign whatever is left now that the preferred ones
+ // have gotten their picks where available.
+ foreach (string typeUri in typeUris) {
+ if (typeUriToAliasMap.ContainsKey(typeUri)) {
+ // this Type URI is already mapped to an alias.
+ continue;
+ }
+
+ assignNewAlias(typeUri);
+ }
+ }
+ /// <summary>
+ /// Sets up aliases for any Type URIs in a dictionary that do not yet have aliases defined,
+ /// and where the given preferred alias is still available.
+ /// </summary>
+ /// <param name="preferredTypeUriToAliases">A dictionary of type URI keys and alias values.</param>
+ public void SetPreferredAliasesWhereNotSet(IDictionary<string, string> preferredTypeUriToAliases) {
+ if (preferredTypeUriToAliases == null) throw new ArgumentNullException("preferredTypeUriToAliases");
+
+ foreach (var pair in preferredTypeUriToAliases) {
+ if (typeUriToAliasMap.ContainsKey(pair.Key)) {
+ // type URI is already mapped
+ continue;
+ }
+
+ if (aliasToTypeUriMap.ContainsKey(pair.Value)) {
+ // alias is already mapped
+ continue;
+ }
+
+ // The type URI and alias are as yet unset, so go ahead and assign them.
+ SetAlias(pair.Value, pair.Key);
+ }
+ }
/// <summary>
/// Gets the Type Uri encoded by a given alias.
@@ -57,6 +112,11 @@ namespace DotNetOpenId.Extensions {
public IEnumerable<string> Aliases {
get { return aliasToTypeUriMap.Keys; }
}
+ /// <summary>
+ /// Returns a value indicating whether an alias has already been assigned to a type URI.
+ /// </summary>
+ /// <param name="alias">The alias in question.</param>
+ /// <returns>True if the alias has already been assigned. False otherwise.</returns>
public bool IsAliasUsed(string alias) {
if (string.IsNullOrEmpty(alias)) throw new ArgumentNullException("alias");
return aliasToTypeUriMap.ContainsKey(alias);
diff --git a/src/DotNetOpenId/Extensions/AttributeExchange/AttributeValues.cs b/src/DotNetOpenId/Extensions/AttributeExchange/AttributeValues.cs
index e537d7f..19abc65 100644
--- a/src/DotNetOpenId/Extensions/AttributeExchange/AttributeValues.cs
+++ b/src/DotNetOpenId/Extensions/AttributeExchange/AttributeValues.cs
@@ -18,15 +18,18 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
internal AttributeValues() {
Values = new List<string>(1);
}
- internal AttributeValues(string typeUri)
- : this() {
+ internal AttributeValues(string typeUri) {
+ if (string.IsNullOrEmpty(typeUri)) throw new ArgumentNullException("typeUri");
TypeUri = typeUri;
+ Values = new List<string>(1);
}
- internal AttributeValues(string typeUri, params string[] values) {
+ /// <summary>
+ /// Instantiates an <see cref="AttributeValues"/> object.
+ /// </summary>
+ public AttributeValues(string typeUri, params string[] values) {
if (string.IsNullOrEmpty(typeUri)) throw new ArgumentNullException("typeUri");
- if (values == null) throw new ArgumentNullException("values");
TypeUri = typeUri;
- Values = values;
+ Values = values ?? new string[0];
}
/// <summary>
diff --git a/src/DotNetOpenId/Extensions/AttributeExchange/FetchRequest.cs b/src/DotNetOpenId/Extensions/AttributeExchange/FetchRequest.cs
index b429643..1b64868 100644
--- a/src/DotNetOpenId/Extensions/AttributeExchange/FetchRequest.cs
+++ b/src/DotNetOpenId/Extensions/AttributeExchange/FetchRequest.cs
@@ -53,6 +53,9 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
#region IExtensionRequest Members
string IExtension.TypeUri { get { return Constants.TypeUri; } }
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
IDictionary<string, string> IExtensionRequest.Serialize(RelyingParty.IAuthenticationRequest authenticationRequest) {
var fields = new Dictionary<string, string> {
@@ -84,7 +87,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
return fields;
}
- bool IExtensionRequest.Deserialize(IDictionary<string, string> fields, DotNetOpenId.Provider.IRequest request) {
+ bool IExtensionRequest.Deserialize(IDictionary<string, string> fields, DotNetOpenId.Provider.IRequest request, string typeUri) {
if (fields == null) return false;
string mode;
fields.TryGetValue("mode", out mode);
@@ -106,17 +109,16 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
allAliases.AddRange(requiredAliases);
allAliases.AddRange(optionalAliases);
if (allAliases.Count == 0) {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Attribute Exchange extension did not provide any aliases in the if_available or required lists.");
+ Logger.Error("Attribute Exchange extension did not provide any aliases in the if_available or required lists.");
return false;
}
AliasManager aliasManager = new AliasManager();
foreach (var alias in allAliases) {
- string typeUri;
- if (fields.TryGetValue("type." + alias, out typeUri)) {
- aliasManager.SetAlias(alias, typeUri);
+ string attributeTypeUri;
+ if (fields.TryGetValue("type." + alias, out attributeTypeUri)) {
+ aliasManager.SetAlias(alias, attributeTypeUri);
AttributeRequest att = new AttributeRequest {
- TypeUri = typeUri,
+ TypeUri = attributeTypeUri,
IsRequired = requiredAliases.Contains(alias),
};
string countString;
@@ -128,8 +130,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
if (int.TryParse(countString, out count) && count > 0) {
att.Count = count;
} else {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("count." + alias + " could not be parsed into a positive integer.");
+ Logger.Error("count." + alias + " could not be parsed into a positive integer.");
}
}
} else {
@@ -137,8 +138,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
}
AddAttribute(att);
} else {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Type URI definition of alias " + alias + " is missing.");
+ Logger.Error("Type URI definition of alias " + alias + " is missing.");
}
}
@@ -149,8 +149,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
List<string> result = new List<string>();
if (string.IsNullOrEmpty(aliasList)) return result;
if (aliasList.Contains(".") || aliasList.Contains("\n")) {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Illegal characters found in Attribute Exchange alias list.");
+ Logger.ErrorFormat("Illegal characters found in Attribute Exchange alias list.");
return result;
}
result.AddRange(aliasList.Split(','));
diff --git a/src/DotNetOpenId/Extensions/AttributeExchange/FetchResponse.cs b/src/DotNetOpenId/Extensions/AttributeExchange/FetchResponse.cs
index e31a8f1..1d58851 100644
--- a/src/DotNetOpenId/Extensions/AttributeExchange/FetchResponse.cs
+++ b/src/DotNetOpenId/Extensions/AttributeExchange/FetchResponse.cs
@@ -55,6 +55,9 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
#region IExtensionResponse Members
string IExtension.TypeUri { get { return Constants.TypeUri; } }
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
IDictionary<string, string> IExtensionResponse.Serialize(Provider.IRequest authenticationRequest) {
var fields = new Dictionary<string, string> {
@@ -87,7 +90,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
}
}
- bool IExtensionResponse.Deserialize(IDictionary<string, string> fields, IAuthenticationResponse response) {
+ bool IExtensionResponse.Deserialize(IDictionary<string, string> fields, IAuthenticationResponse response, string typeUri) {
if (fields == null) return false;
string mode;
fields.TryGetValue("mode", out mode);
@@ -113,9 +116,8 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
bool countSent = false;
string countString;
if (fields.TryGetValue("count." + alias, out countString)) {
- if (!int.TryParse(countString, out count) || count <= 0) {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Failed to parse count.{0} value to a positive integer.");
+ if (!int.TryParse(countString, out count) || count < 0) {
+ Logger.ErrorFormat("Failed to parse count.{0} value to a non-negative integer.", alias);
continue;
}
countSent = true;
@@ -126,8 +128,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
if (fields.TryGetValue(string.Format(CultureInfo.InvariantCulture, "value.{0}.{1}", alias, i), out value)) {
att.Values.Add(value);
} else {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Missing value for attribute '{0}'.", att.TypeUri);
+ Logger.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri);
continue;
}
}
@@ -136,8 +137,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
if (fields.TryGetValue("value." + alias, out value))
att.Values.Add(value);
else {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Missing value for attribute '{0}'.", att.TypeUri);
+ Logger.ErrorFormat("Missing value for attribute '{0}'.", att.TypeUri);
continue;
}
}
@@ -152,8 +152,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
if (!pair.Key.StartsWith("type.", StringComparison.Ordinal)) continue;
string alias = pair.Key.Substring(5);
if (alias.IndexOfAny(new[] { '.', ',', ':' }) >= 0) {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Illegal characters in alias name '{0}'.", alias);
+ Logger.ErrorFormat("Illegal characters in alias name '{0}'.", alias);
continue;
}
aliasManager.SetAlias(alias, pair.Value);
diff --git a/src/DotNetOpenId/Extensions/AttributeExchange/StoreRequest.cs b/src/DotNetOpenId/Extensions/AttributeExchange/StoreRequest.cs
index a7e2199..6b5ce2b 100644
--- a/src/DotNetOpenId/Extensions/AttributeExchange/StoreRequest.cs
+++ b/src/DotNetOpenId/Extensions/AttributeExchange/StoreRequest.cs
@@ -30,6 +30,13 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
attributesProvided.Add(attribute);
}
/// <summary>
+ /// Used by the Relying Party to add a given attribute with one or more values
+ /// to the request for storage.
+ /// </summary>
+ public void AddAttribute(string typeUri, params string[] values) {
+ AddAttribute(new AttributeValues(typeUri, values));
+ }
+ /// <summary>
/// Used by the Provider to gets the value(s) associated with a given attribute
/// that should be stored.
/// </summary>
@@ -46,6 +53,9 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
#region IExtensionRequest Members
string IExtension.TypeUri { get { return Constants.TypeUri; } }
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
IDictionary<string, string> IExtensionRequest.Serialize(RelyingParty.IAuthenticationRequest authenticationRequest) {
var fields = new Dictionary<string, string> {
@@ -57,7 +67,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
return fields;
}
- bool IExtensionRequest.Deserialize(IDictionary<string, string> fields, DotNetOpenId.Provider.IRequest request) {
+ bool IExtensionRequest.Deserialize(IDictionary<string, string> fields, DotNetOpenId.Provider.IRequest request, string typeUri) {
if (fields == null) return false;
string mode;
fields.TryGetValue("mode", out mode);
diff --git a/src/DotNetOpenId/Extensions/AttributeExchange/StoreResponse.cs b/src/DotNetOpenId/Extensions/AttributeExchange/StoreResponse.cs
index a4e9cf1..acc0d6b 100644
--- a/src/DotNetOpenId/Extensions/AttributeExchange/StoreResponse.cs
+++ b/src/DotNetOpenId/Extensions/AttributeExchange/StoreResponse.cs
@@ -24,6 +24,9 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
#region IExtensionResponse Members
string IExtension.TypeUri { get { return Constants.TypeUri; } }
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
IDictionary<string, string> IExtensionResponse.Serialize(Provider.IRequest authenticationRequest) {
var fields = new Dictionary<string, string> {
@@ -35,7 +38,7 @@ namespace DotNetOpenId.Extensions.AttributeExchange {
return fields;
}
- bool IExtensionResponse.Deserialize(IDictionary<string, string> fields, IAuthenticationResponse response) {
+ bool IExtensionResponse.Deserialize(IDictionary<string, string> fields, IAuthenticationResponse response, string typeUri) {
if (fields == null) return false;
string mode;
if (!fields.TryGetValue("mode", out mode)) return false;
diff --git a/src/DotNetOpenId/Extensions/ExtensionManager.cs b/src/DotNetOpenId/Extensions/ExtensionManager.cs
new file mode 100644
index 0000000..9b197c9
--- /dev/null
+++ b/src/DotNetOpenId/Extensions/ExtensionManager.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.Extensions {
+ internal class ExtensionManager {
+ /// <summary>
+ /// A list of request extensions that may be enumerated over for logging purposes.
+ /// </summary>
+ internal static Dictionary<IExtensionRequest, string> RequestExtensions = new Dictionary<IExtensionRequest, string> {
+ {new AttributeExchange.FetchRequest(), "AX fetch"},
+ {new AttributeExchange.StoreRequest(), "AX store"},
+ {new ProviderAuthenticationPolicy.PolicyRequest(), "PAPE"},
+ {new SimpleRegistration.ClaimsRequest(), "sreg"},
+ };
+ //internal static List<IExtensionResponse> ResponseExtensions = new List<IExtensionResponse> {
+ // new AttributeExchange.FetchResponse(),
+ // new AttributeExchange.StoreResponse(),
+ // new ProviderAuthenticationPolicy.PolicyResponse(),
+ // new SimpleRegistration.ClaimsResponse(),
+ //};
+ }
+}
diff --git a/src/DotNetOpenId/Extensions/IClientScriptExtensionResponse.cs b/src/DotNetOpenId/Extensions/IClientScriptExtensionResponse.cs
new file mode 100644
index 0000000..587014d
--- /dev/null
+++ b/src/DotNetOpenId/Extensions/IClientScriptExtensionResponse.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId.Extensions {
+ /// <summary>
+ /// An interface that OpenID extensions can implement to allow authentication response
+ /// messages with included extensions to be processed by Javascript on the user agent.
+ /// </summary>
+ public interface IClientScriptExtensionResponse : IExtension {
+ /// <summary>
+ /// Reads the extension information on an authentication response from the provider.
+ /// </summary>
+ /// <param name="fields">The fields belonging to the extension.</param>
+ /// <param name="response">The incoming OpenID response carrying the extension.</param>
+ /// <param name="typeUri">The actual extension TypeUri that was recognized in the message.</param>
+ /// <returns>
+ /// A Javascript snippet that when executed on the user agent returns an object with
+ /// the information deserialized from the extension response.
+ /// </returns>
+ /// <remarks>
+ /// This method is called <b>before</b> the signature on the assertion response has been
+ /// verified. Therefore all information in these fields should be assumed unreliable
+ /// and potentially falsified.
+ /// </remarks>
+ string InitializeJavaScriptData(IDictionary<string, string> fields, IAuthenticationResponse response, string typeUri);
+ }
+}
diff --git a/src/DotNetOpenId/Extensions/IExtension.cs b/src/DotNetOpenId/Extensions/IExtension.cs
index 2450a8a..e43565a 100644
--- a/src/DotNetOpenId/Extensions/IExtension.cs
+++ b/src/DotNetOpenId/Extensions/IExtension.cs
@@ -11,6 +11,22 @@ namespace DotNetOpenId.Extensions {
/// Gets the TypeURI the extension uses in the OpenID protocol and in XRDS advertisements.
/// </summary>
string TypeUri { get; }
+ /// <summary>
+ /// Additional TypeURIs that are supported by this extension, in preferred order.
+ /// May be empty if none other than <see cref="TypeUri"/> is supported, but
+ /// should not be null.
+ /// </summary>
+ /// <remarks>
+ /// Useful for reading in messages with an older version of an extension.
+ /// The value in the <see cref="TypeUri"/> property is always checked before
+ /// trying this list.
+ /// If you do support multiple versions of an extension using this method,
+ /// consider adding a CreateResponse method to your request extension class
+ /// so that the response can have the context it needs to remain compatible
+ /// given the version of the extension in the request message.
+ /// The <see cref="SimpleRegistration.ClaimsRequest.CreateResponse"/> for an example.
+ /// </remarks>
+ IEnumerable<string> AdditionalSupportedTypeUris { get; }
}
/// <summary>
@@ -26,8 +42,14 @@ namespace DotNetOpenId.Extensions {
/// <summary>
/// Reads the extension information on an authentication request to the provider.
/// </summary>
- /// <returns>True if the extension found any of its parameters in the request, false otherwise.</returns>
- bool Deserialize(IDictionary<string, string> fields, Provider.IRequest request);
+ /// <param name="fields">The fields belonging to the extension.</param>
+ /// <param name="request">The incoming OpenID request carrying the extension.</param>
+ /// <param name="typeUri">The actual extension TypeUri that was recognized in the message.</param>
+ /// <returns>
+ /// True if the extension found a valid set of recognized parameters in the request,
+ /// false otherwise.
+ /// </returns>
+ bool Deserialize(IDictionary<string, string> fields, Provider.IRequest request, string typeUri);
}
/// <summary>
@@ -43,7 +65,13 @@ namespace DotNetOpenId.Extensions {
/// <summary>
/// Reads a Provider's response for extension values.
/// </summary>
- /// <returns>True if the extension found any of its parameters in the response.</returns>
- bool Deserialize(IDictionary<string, string> fields, RelyingParty.IAuthenticationResponse response);
+ /// <param name="fields">The fields belonging to the extension.</param>
+ /// <param name="response">The incoming OpenID response carrying the extension.</param>
+ /// <param name="typeUri">The actual extension TypeUri that was recognized in the message.</param>
+ /// <returns>
+ /// True if the extension found a valid set of recognized parameters in the response,
+ /// false otherwise.
+ /// </returns>
+ bool Deserialize(IDictionary<string, string> fields, RelyingParty.IAuthenticationResponse response, string typeUri);
}
}
diff --git a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs
index 517525f..f505a9b 100644
--- a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs
+++ b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/AuthenticationPolicies.cs
@@ -13,6 +13,13 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// </remarks>
public static class AuthenticationPolicies {
/// <summary>
+ /// Used in a PAPE response to indicate that no PAPE authentication policies could be satisfied.
+ /// </summary>
+ /// <remarks>
+ /// Used internally by the PAPE extension, so that users don't have to know about it.
+ /// </remarks>
+ internal const string None = "http://schemas.openid.net/pape/policies/2007/06/none";
+ /// <summary>
/// An authentication mechanism where the End User does not provide a shared secret to a party potentially under the control of the Relying Party. (Note that the potentially malicious Relying Party controls where the User-Agent is redirected to and thus may not send it to the End User's actual OpenID Provider).
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Phishing")]
diff --git a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs
index 13fc4cf..395ea36 100644
--- a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs
+++ b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/Constants.cs
@@ -12,6 +12,24 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// </summary>
internal const string TypeUri = "http://specs.openid.net/extensions/pape/1.0";
/// <summary>
+ /// The namespace alias to use for OpenID 1.x interop, where aliases are not defined in the message.
+ /// </summary>
+ internal const string pape_compatibility_alias = "pape";
+ /// <summary>
+ /// The string to prepend on an Auth Level Type alias definition.
+ /// </summary>
+ internal const string AuthLevelNamespaceDeclarationPrefix = "auth_level.ns.";
+
+ internal static class AuthenticationLevels {
+ internal static readonly IDictionary<string, string> PreferredTypeUriToAliasMap = new Dictionary<string, string> {
+ { NistTypeUri, nist_compatibility_alias },
+ };
+
+ internal const string nist_compatibility_alias = "nist";
+ internal const string NistTypeUri = "http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf";
+ }
+
+ /// <summary>
/// Parameters to be included with PAPE requests.
/// </summary>
internal static class RequestParameters {
@@ -31,6 +49,10 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// If no policies are requested, the RP may be interested in other information such as the authentication age.
/// </remarks>
internal const string PreferredAuthPolicies = "preferred_auth_policies";
+ /// <summary>
+ /// The space separated list of the name spaces of the custom Assurance Level that RP requests, in the order of its preference.
+ /// </summary>
+ internal const string PreferredAuthLevelTypes = "preferred_auth_level_types";
}
/// <summary>
/// Parameters to be included with PAPE responses.
@@ -58,13 +80,11 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// </remarks>
internal const string AuthTime = "auth_time";
/// <summary>
- /// Optional. The Assurance Level as defined by the National Institute of Standards and Technology (NIST) in Special Publication 800-63 (Burr, W., Dodson, D., and W. Polk, Ed., “Electronic Authentication Guideline,” April 2006.) [NIST_SP800‑63] corresponding to the authentication method and policies employed by the OP when authenticating the End User.
+ /// The first part of a parameter name that gives the custom string value for
+ /// the assurance level. The second part of the parameter name is the alias for
+ /// that assurance level.
/// </summary>
- /// <value>Integer value between 0 and 4 inclusive.</value>
- /// <remarks>
- /// Level 0 is not an assurance level defined by NIST, but rather SHOULD be used to signify that the OP recognizes the parameter and the End User authentication did not meet the requirements of Level 1. See Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels.
- /// </remarks>
- internal const string NistAuthLevel = "nist_auth_level";
+ internal const string AuthLevelAliasPrefix = "auth_level.";
}
}
}
diff --git a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs
index 2afc118..6358294 100644
--- a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs
+++ b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/NistAssuranceLevel.cs
@@ -13,6 +13,8 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// before asserting or interpreting what these levels signify, notwithstanding
/// the brief summaries attached to each level in DotNetOpenId documentation.
/// http://csrc.nist.gov/publications/nistpubs/800-63/SP800-63V1_0_2.pdf
+ ///
+ /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels.
/// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Nist")]
public enum NistAssuranceLevel {
diff --git a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs
index 6493db1..eb78e1f 100644
--- a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs
+++ b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyRequest.cs
@@ -14,6 +14,7 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// </summary>
public PolicyRequest() {
PreferredPolicies = new List<string>(1);
+ PreferredAuthLevelTypes = new List<string>(1);
}
/// <summary>
@@ -33,6 +34,11 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
public IList<string> PreferredPolicies { get; private set; }
/// <summary>
+ /// Zero or more name spaces of the custom Assurance Level the RP requests, in the order of its preference.
+ /// </summary>
+ public IList<string> PreferredAuthLevelTypes { get; private set; }
+
+ /// <summary>
/// Tests equality between two <see cref="PolicyRequest"/> instances.
/// </summary>
public override bool Equals(object obj) {
@@ -43,6 +49,10 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
foreach(string policy in PreferredPolicies) {
if (!other.PreferredPolicies.Contains(policy)) return false;
}
+ if (PreferredAuthLevelTypes.Count != other.PreferredAuthLevelTypes.Count) return false;
+ foreach (string authLevel in PreferredAuthLevelTypes) {
+ if (!other.PreferredAuthLevelTypes.Contains(authLevel)) return false;
+ }
return true;
}
@@ -66,10 +76,23 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
// Even if empty, this parameter is required as part of the request message.
fields.Add(Constants.RequestParameters.PreferredAuthPolicies, SerializePolicies(PreferredPolicies));
+ if (PreferredAuthLevelTypes.Count > 0) {
+ AliasManager authLevelAliases = new AliasManager();
+ authLevelAliases.AssignAliases(PreferredAuthLevelTypes, Constants.AuthenticationLevels.PreferredTypeUriToAliasMap);
+
+ // Add a definition for each Auth Level Type alias.
+ foreach (string alias in authLevelAliases.Aliases) {
+ fields.Add(Constants.AuthLevelNamespaceDeclarationPrefix + alias, authLevelAliases.ResolveAlias(alias));
+ }
+
+ // Now use the aliases for those type URIs to list a preferred order.
+ fields.Add(Constants.RequestParameters.PreferredAuthLevelTypes, SerializeAuthLevels(PreferredAuthLevelTypes, authLevelAliases));
+ }
+
return fields;
}
- bool IExtensionRequest.Deserialize(IDictionary<string, string> fields, DotNetOpenId.Provider.IRequest request) {
+ bool IExtensionRequest.Deserialize(IDictionary<string, string> fields, DotNetOpenId.Provider.IRequest request, string typeUri) {
if (fields == null) return false;
if (!fields.ContainsKey(Constants.RequestParameters.PreferredAuthPolicies)) return false;
@@ -84,6 +107,16 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
PreferredPolicies.Add(policy);
}
+ PreferredAuthLevelTypes.Clear();
+ AliasManager authLevelAliases = FindIncomingAliases(fields);
+ string preferredAuthLevelAliases;
+ if (fields.TryGetValue(Constants.RequestParameters.PreferredAuthLevelTypes, out preferredAuthLevelAliases)) {
+ foreach (string authLevelAlias in preferredAuthLevelAliases.Split(' ')) {
+ if (authLevelAlias.Length == 0) continue;
+ PreferredAuthLevelTypes.Add(authLevelAliases.ResolveAlias(authLevelAlias));
+ }
+ }
+
return true;
}
@@ -95,23 +128,61 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
get { return Constants.TypeUri; }
}
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
+
#endregion
static internal string SerializePolicies(IList<string> policies) {
- Debug.Assert(policies != null);
- StringBuilder policyList = new StringBuilder();
- foreach (string policy in GetUniqueItems(policies)) {
- if (policy.Contains(" ")) {
+ return ConcatenateListOfElements(policies);
+ }
+
+ private static string SerializeAuthLevels(IList<string> preferredAuthLevelTypes, AliasManager aliases) {
+ var aliasList = new List<string>();
+ foreach (string typeUri in preferredAuthLevelTypes) {
+ aliasList.Add(aliases.GetAlias(typeUri));
+ }
+
+ return ConcatenateListOfElements(aliasList);
+ }
+
+ /// <summary>
+ /// Looks at the incoming fields and figures out what the aliases and name spaces for auth level types are.
+ /// </summary>
+ internal static AliasManager FindIncomingAliases(IDictionary<string, string> fields) {
+ AliasManager aliasManager = new AliasManager();
+
+ foreach (var pair in fields) {
+ if (!pair.Key.StartsWith(Constants.AuthLevelNamespaceDeclarationPrefix, StringComparison.Ordinal)) {
+ continue;
+ }
+
+ string alias = pair.Key.Substring(Constants.AuthLevelNamespaceDeclarationPrefix.Length);
+ aliasManager.SetAlias(alias, pair.Value);
+ }
+
+ aliasManager.SetPreferredAliasesWhereNotSet(Constants.AuthenticationLevels.PreferredTypeUriToAliasMap);
+
+ return aliasManager;
+ }
+
+ internal static string ConcatenateListOfElements(IList<string> values) {
+ Debug.Assert(values != null);
+ StringBuilder valuesList = new StringBuilder();
+ foreach (string value in GetUniqueItems(values)) {
+ if (value.Contains(" ")) {
throw new FormatException(string.Format(CultureInfo.CurrentCulture,
- Strings.InvalidUri, policy));
+ Strings.InvalidUri, value));
}
- policyList.Append(policy);
- policyList.Append(" ");
+ valuesList.Append(value);
+ valuesList.Append(" ");
}
- if (policyList.Length > 0)
- policyList.Length -= 1; // remove trailing space
- return policyList.ToString();
+ if (valuesList.Length > 0)
+ valuesList.Length -= 1; // remove trailing space
+ return valuesList.ToString();
}
+
static internal IEnumerable<T> GetUniqueItems<T>(IList<T> list) {
List<T> itemsSeen = new List<T>(list.Count);
foreach (T item in list) {
diff --git a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
index a30c873..0f5922f 100644
--- a/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
+++ b/src/DotNetOpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
@@ -17,6 +17,7 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
/// </summary>
public PolicyResponse() {
ActualPolicies = new List<string>(1);
+ AssuranceLevels = new Dictionary<string, string>(1);
}
/// <summary>
@@ -46,13 +47,37 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
}
}
}
+
+ /// <summary>
+ /// Optional. The Assurance Level as defined by the National Institute of Standards and Technology (NIST) in Special Publication 800-63 (Burr, W., Dodson, D., and W. Polk, Ed., “Electronic Authentication Guideline,” April 2006.) [NIST_SP800‑63] corresponding to the authentication method and policies employed by the OP when authenticating the End User. /// </summary>
+ /// <remarks> /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels. /// </remarks>
+ public NistAssuranceLevel? NistAssuranceLevel {
+ get {
+ string levelString;
+ if (AssuranceLevels.TryGetValue(Constants.AuthenticationLevels.NistTypeUri, out levelString)) {
+ return (NistAssuranceLevel)Enum.Parse(typeof(NistAssuranceLevel), levelString);
+ } else {
+ return null;
+ }
+ }
+ set {
+ if (value != null) {
+ AssuranceLevels[Constants.AuthenticationLevels.NistTypeUri] = ((int)value).ToString(CultureInfo.InvariantCulture);
+ } else {
+ AssuranceLevels.Remove(Constants.AuthenticationLevels.NistTypeUri);
+ }
+ }
+ }
+
/// <summary>
- /// Optional. The Assurance Level as defined by the National Institute of Standards and Technology (NIST) in Special Publication 800-63 (Burr, W., Dodson, D., and W. Polk, Ed., “Electronic Authentication Guideline,” April 2006.) [NIST_SP800‑63] corresponding to the authentication method and policies employed by the OP when authenticating the End User.
+ /// Gets a dictionary where keys are the authentication level type URIs and
+ /// the values are the per authentication level defined custom value.
/// </summary>
/// <remarks>
- /// See PAPE spec Appendix A.1.2 (NIST Assurance Levels) for high-level example classifications of authentication methods within the defined levels.
+ /// A very common key is <see cref="Constants.AuthenticationLevels.NistTypeUri"/>
+ /// and values for this key are available in <see cref="NistAssuranceLevel"/>.
/// </remarks>
- public NistAssuranceLevel? NistAssuranceLevel { get; set; }
+ public IDictionary<string, string> AssuranceLevels { get; private set; }
/// <summary>
/// Tests equality between two <see cref="PolicyResponse"/> instances.
@@ -61,7 +86,10 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
PolicyResponse other = obj as PolicyResponse;
if (other == null) return false;
if (AuthenticationTimeUtc != other.AuthenticationTimeUtc) return false;
- if (NistAssuranceLevel != other.NistAssuranceLevel) return false;
+ if (AssuranceLevels.Count != other.AssuranceLevels.Count) return false;
+ foreach (var pair in AssuranceLevels) {
+ if (!other.AssuranceLevels.Contains(pair)) return false;
+ }
if (ActualPolicies.Count != other.ActualPolicies.Count) return false;
foreach (string policy in ActualPolicies) {
if (!other.ActualPolicies.Contains(policy)) return false;
@@ -81,25 +109,37 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
IDictionary<string, string> IExtensionResponse.Serialize(DotNetOpenId.Provider.IRequest authenticationRequest) {
var fields = new Dictionary<string, string>();
- fields.Add(Constants.ResponseParameters.AuthPolicies, PolicyRequest.SerializePolicies(ActualPolicies));
+ fields.Add(Constants.ResponseParameters.AuthPolicies, SerializePolicies(ActualPolicies));
if (AuthenticationTimeUtc.HasValue) {
fields.Add(Constants.ResponseParameters.AuthTime, AuthenticationTimeUtc.Value.ToUniversalTime().ToString(PermissibleDateTimeFormats[0], CultureInfo.InvariantCulture));
}
- if (NistAssuranceLevel.HasValue) {
- fields.Add(Constants.ResponseParameters.NistAuthLevel, ((int)NistAssuranceLevel).ToString(CultureInfo.InvariantCulture));
+
+ if (AssuranceLevels.Count > 0) {
+ AliasManager aliases = new AliasManager();
+ aliases.AssignAliases(AssuranceLevels.Keys, Constants.AuthenticationLevels.PreferredTypeUriToAliasMap);
+
+ // Add a definition for each Auth Level Type alias.
+ foreach (string alias in aliases.Aliases) {
+ fields.Add(Constants.AuthLevelNamespaceDeclarationPrefix + alias, aliases.ResolveAlias(alias));
+ }
+
+ // Now use the aliases for those type URIs to list the individual values.
+ foreach (var pair in AssuranceLevels) {
+ fields.Add(Constants.ResponseParameters.AuthLevelAliasPrefix + aliases.GetAlias(pair.Key), pair.Value);
+ }
}
return fields;
}
- bool IExtensionResponse.Deserialize(IDictionary<string, string> fields, DotNetOpenId.RelyingParty.IAuthenticationResponse response) {
+ bool IExtensionResponse.Deserialize(IDictionary<string, string> fields, DotNetOpenId.RelyingParty.IAuthenticationResponse response, string typeUri) {
if (fields == null) return false;
if (!fields.ContainsKey(Constants.ResponseParameters.AuthPolicies)) return false;
ActualPolicies.Clear();
string[] actualPolicies = fields[Constants.ResponseParameters.AuthPolicies].Split(' ');
foreach (string policy in actualPolicies) {
- if (policy.Length > 0)
+ if (policy.Length > 0 && policy != AuthenticationPolicies.None)
ActualPolicies.Add(policy);
}
@@ -111,22 +151,18 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
authDateTime.Kind == DateTimeKind.Utc) { // may be unspecified per our option above
AuthenticationTimeUtc = authDateTime;
} else {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Invalid format for {0} parameter: {1}",
- Constants.ResponseParameters.AuthTime, authTime);
+ Logger.ErrorFormat("Invalid format for {0} parameter: {1}",
+ Constants.ResponseParameters.AuthTime, authTime);
}
}
- NistAssuranceLevel = null;
- string nistAuthLevel;
- if (fields.TryGetValue(Constants.ResponseParameters.NistAuthLevel, out nistAuthLevel)) {
- int nistAuthLevelNumber;
- if (int.TryParse(nistAuthLevel, out nistAuthLevelNumber) &&
- nistAuthLevelNumber >= 0 && nistAuthLevelNumber <= 4) {
- NistAssuranceLevel = (NistAssuranceLevel)nistAuthLevelNumber;
- } else {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Invalid NIST level.");
+ AssuranceLevels.Clear();
+ AliasManager authLevelAliases = PolicyRequest.FindIncomingAliases(fields);
+ foreach (string authLevelAlias in authLevelAliases.Aliases) {
+ string authValue;
+ if (fields.TryGetValue(Constants.ResponseParameters.AuthLevelAliasPrefix + authLevelAlias, out authValue)) {
+ string authLevelType = authLevelAliases.ResolveAlias(authLevelAlias);
+ AssuranceLevels[authLevelType] = authValue;
}
}
@@ -141,6 +177,18 @@ namespace DotNetOpenId.Extensions.ProviderAuthenticationPolicy {
get { return Constants.TypeUri; }
}
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
+
#endregion
+
+ static internal string SerializePolicies(IList<string> policies) {
+ if (policies.Count == 0) {
+ return AuthenticationPolicies.None;
+ } else {
+ return PolicyRequest.ConcatenateListOfElements(policies);
+ }
+ }
}
}
diff --git a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsRequest.cs b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
index b8a8597..aab0608 100644
--- a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
+++ b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
@@ -97,7 +97,7 @@ namespace DotNetOpenId.Extensions.SimpleRegistration {
TimeZone = requestLevel;
break;
default:
- Trace.TraceWarning("OpenIdProfileRequest.SetProfileRequestFromList: Unrecognized field name '{0}'.", field);
+ Logger.WarnFormat("OpenIdProfileRequest.SetProfileRequestFromList: Unrecognized field name '{0}'.", field);
break;
}
}
@@ -128,9 +128,18 @@ namespace DotNetOpenId.Extensions.SimpleRegistration {
#region IExtensionRequest Members
string IExtension.TypeUri { get { return Constants.sreg_ns; } }
+ static readonly string[] additionalTypeUris = new string[] {
+ Constants.sreg_ns10,
+ Constants.sreg_ns11other,
+ };
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return additionalTypeUris; }
+ }
- bool IExtensionRequest.Deserialize(IDictionary<string, string> args, IRequest request) {
+ bool IExtensionRequest.Deserialize(IDictionary<string, string> args, IRequest request, string typeUri) {
if (args == null) return false;
+ Debug.Assert(!string.IsNullOrEmpty(typeUri));
+ typeUriDeserializedFrom = typeUri;
string policyUrl;
if (args.TryGetValue(Constants.policy_url, out policyUrl)
@@ -168,10 +177,21 @@ namespace DotNetOpenId.Extensions.SimpleRegistration {
}
#endregion
+ string typeUriDeserializedFrom;
+ /// <summary>
+ /// Prepares a Simple Registration response extension that is compatible with the
+ /// version of Simple Registration used in the request message.
+ /// </summary>
+ public ClaimsResponse CreateResponse() {
+ if (typeUriDeserializedFrom == null) {
+ throw new InvalidOperationException(Strings.CallDeserializeBeforeCreateResponse);
+ }
+ return new ClaimsResponse(typeUriDeserializedFrom);
+ }
+
/// <summary>
/// Renders the requested information as a string.
/// </summary>
- /// <returns></returns>
public override string ToString() {
return string.Format(CultureInfo.CurrentCulture, @"Nickname = '{0}'
Email = '{1}'
diff --git a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
index 88f0b35..a9b0cbd 100644
--- a/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
+++ b/src/DotNetOpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
@@ -6,13 +6,12 @@
********************************************************/
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Net.Mail;
-using DotNetOpenId.Extensions;
+using System.Text;
using System.Xml.Serialization;
using DotNetOpenId.RelyingParty;
-using DotNetOpenId.Provider;
-using System.Collections.Generic;
namespace DotNetOpenId.Extensions.SimpleRegistration
{
@@ -22,8 +21,22 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
/// authenticating user.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2218:OverrideGetHashCodeOnOverridingEquals"), Serializable()]
- public sealed class ClaimsResponse : IExtensionResponse
+ public sealed class ClaimsResponse : IExtensionResponse, IClientScriptExtensionResponse
{
+ string typeUriToUse;
+
+ /// <summary>
+ /// Creates an instance of the <see cref="ClaimsResponse"/> class.
+ /// </summary>
+ [Obsolete("Use ClaimsRequest.CreateResponse() instead.")]
+ public ClaimsResponse() : this(Constants.sreg_ns) {
+ }
+
+ internal ClaimsResponse(string typeUriToUse) {
+ if (string.IsNullOrEmpty(typeUriToUse)) throw new ArgumentNullException("typeUriToUse");
+ this.typeUriToUse = typeUriToUse;
+ }
+
/// <summary>
/// The nickname the user goes by.
/// </summary>
@@ -39,7 +52,7 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
{
get
{
- if (Email == null) return null;
+ if (string.IsNullOrEmpty(Email)) return null;
if (string.IsNullOrEmpty(FullName))
return new MailAddress(Email);
else
@@ -104,7 +117,10 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
public string TimeZone { get; set; }
#region IExtensionResponse Members
- string IExtension.TypeUri { get { return Constants.sreg_ns; } }
+ string IExtension.TypeUri { get { return typeUriToUse; } }
+ IEnumerable<string> IExtension.AdditionalSupportedTypeUris {
+ get { return new string[0]; }
+ }
/// <summary>
/// Adds the values of this struct to an authentication response being prepared
@@ -148,7 +164,7 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
return fields;
}
- bool IExtensionResponse.Deserialize(IDictionary<string, string> sreg, IAuthenticationResponse response) {
+ bool IExtensionResponse.Deserialize(IDictionary<string, string> sreg, IAuthenticationResponse response, string typeUri) {
if (sreg == null) return false;
string nickname, email, fullName, dob, genderString, postalCode, country, language, timeZone;
BirthDate = null;
@@ -184,6 +200,52 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
#endregion
+ #region IClientScriptExtension Members
+
+ static string createAddFieldJS(string propertyName, string value) {
+ return string.Format(CultureInfo.InvariantCulture, "{0}: {1},",
+ propertyName, Util.GetSafeJavascriptValue(value));
+ }
+
+ string IClientScriptExtensionResponse.InitializeJavaScriptData(IDictionary<string, string> sreg, IAuthenticationResponse response, string typeUri) {
+ StringBuilder builder = new StringBuilder();
+ builder.Append("{ ");
+
+ string nickname, email, fullName, dob, genderString, postalCode, country, language, timeZone;
+ if (sreg.TryGetValue(Constants.nickname, out nickname)) {
+ builder.Append(createAddFieldJS(Constants.nickname, nickname));
+ }
+ if (sreg.TryGetValue(Constants.email, out email)) {
+ builder.Append(createAddFieldJS(Constants.email, email));
+ }
+ if (sreg.TryGetValue(Constants.fullname, out fullName)) {
+ builder.Append(createAddFieldJS(Constants.fullname, fullName));
+ }
+ if (sreg.TryGetValue(Constants.dob, out dob)) {
+ builder.Append(createAddFieldJS(Constants.dob, dob));
+ }
+ if (sreg.TryGetValue(Constants.gender, out genderString)) {
+ builder.Append(createAddFieldJS(Constants.gender, genderString));
+ }
+ if (sreg.TryGetValue(Constants.postcode, out postalCode)) {
+ builder.Append(createAddFieldJS(Constants.postcode, postalCode));
+ }
+ if (sreg.TryGetValue(Constants.country, out country)) {
+ builder.Append(createAddFieldJS(Constants.country, country));
+ }
+ if (sreg.TryGetValue(Constants.language, out language)) {
+ builder.Append(createAddFieldJS(Constants.language, language));
+ }
+ if (sreg.TryGetValue(Constants.timezone, out timeZone)) {
+ builder.Append(createAddFieldJS(Constants.timezone, timeZone));
+ }
+ if (builder[builder.Length - 1] == ',') builder.Length -= 1;
+ builder.Append("}");
+ return builder.ToString();
+ }
+
+ #endregion
+
/// <summary>
/// Tests equality of two <see cref="ClaimsResponse"/> objects.
/// </summary>
@@ -223,6 +285,5 @@ namespace DotNetOpenId.Extensions.SimpleRegistration
if (one == null ^ other == null) return false;
return one.Equals(other);
}
-
}
} \ No newline at end of file
diff --git a/src/DotNetOpenId/Extensions/SimpleRegistration/Constants.cs b/src/DotNetOpenId/Extensions/SimpleRegistration/Constants.cs
index fc4066e..8aa9591 100644
--- a/src/DotNetOpenId/Extensions/SimpleRegistration/Constants.cs
+++ b/src/DotNetOpenId/Extensions/SimpleRegistration/Constants.cs
@@ -7,8 +7,9 @@ namespace DotNetOpenId.Extensions.SimpleRegistration {
/// Simple Registration constants
/// </summary>
internal static class Constants {
- internal const string TypeUri = "http://openid.net/sreg/1.0";
internal const string sreg_ns = "http://openid.net/extensions/sreg/1.1";
+ internal const string sreg_ns10 = "http://openid.net/sreg/1.0";
+ internal const string sreg_ns11other = "http://openid.net/sreg/1.1";
internal const string sreg_compatibility_alias = "sreg";
internal const string policy_url = "policy_url";
internal const string optional = "optional";
diff --git a/src/DotNetOpenId/GlobalSuppressions.cs b/src/DotNetOpenId/GlobalSuppressions.cs
index a3b36f7..e0c7ca7 100644
--- a/src/DotNetOpenId/GlobalSuppressions.cs
+++ b/src/DotNetOpenId/GlobalSuppressions.cs
@@ -80,3 +80,4 @@
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Scope = "member", Target = "DotNetOpenId.UntrustedWebRequest.#WhitelistHostsRegEx")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Scope = "member", Target = "DotNetOpenId.UntrustedWebRequest.#WhitelistHostsRegex")]
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Whitelist", Scope = "member", Target = "DotNetOpenId.UntrustedWebRequest.#WhitelistHosts")]
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xri", Scope = "type", Target = "DotNetOpenId.XriIdentifier")]
diff --git a/src/DotNetOpenId/HmacSha1Association.cs b/src/DotNetOpenId/HmacSha1Association.cs
deleted file mode 100644
index da344f1..0000000
--- a/src/DotNetOpenId/HmacSha1Association.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Security.Cryptography;
-using System.Collections.Generic;
-using System.Diagnostics;
-
-namespace DotNetOpenId {
- internal class HmacSha1Association : Association {
-
- public HmacSha1Association(string handle, byte[] secret, TimeSpan totalLifeLength)
- : base(handle, secret, totalLifeLength, DateTime.UtcNow) {
- Debug.Assert(secret.Length == CryptUtil.Sha1.HashSize / 8);
- }
-
- internal override string GetAssociationType(Protocol protocol) {
- return protocol.Args.SignatureAlgorithm.HMAC_SHA1;
- }
-
- protected override HashAlgorithm CreateHasher() {
- return new HMACSHA1(SecretKey);
- }
- }
-} \ No newline at end of file
diff --git a/src/DotNetOpenId/HmacSha256Association.cs b/src/DotNetOpenId/HmacSha256Association.cs
deleted file mode 100644
index fddc811..0000000
--- a/src/DotNetOpenId/HmacSha256Association.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Security.Cryptography;
-using System.Diagnostics;
-
-namespace DotNetOpenId {
- class HmacSha256Association : Association {
- public HmacSha256Association(string handle, byte[] secret, TimeSpan totalLifeLength)
- : base(handle, secret, totalLifeLength, DateTime.UtcNow) {
- Debug.Assert(secret.Length == CryptUtil.Sha256.HashSize / 8);
- }
-
- protected override HashAlgorithm CreateHasher() {
- return new HMACSHA256(SecretKey);
- }
-
- internal override string GetAssociationType(Protocol protocol) {
- return protocol.Args.SignatureAlgorithm.HMAC_SHA256;
- }
- }
-}
diff --git a/src/DotNetOpenId/HmacShaAssociation.cs b/src/DotNetOpenId/HmacShaAssociation.cs
new file mode 100644
index 0000000..a754aca
--- /dev/null
+++ b/src/DotNetOpenId/HmacShaAssociation.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Diagnostics;
+using System.Security.Cryptography;
+
+namespace DotNetOpenId {
+ internal class HmacShaAssociation : Association {
+
+ class HmacSha {
+ internal Util.Func<Protocol, string> GetAssociationType;
+ internal Util.Func<byte[], HashAlgorithm> CreateHasher;
+ internal HashAlgorithm BaseHashAlgorithm;
+ /// <summary>
+ /// The size of the hash (in bytes).
+ /// </summary>
+ internal int SecretLength { get { return BaseHashAlgorithm.HashSize / 8; } }
+ }
+ static HmacSha[] HmacShaAssociationTypes = {
+ new HmacSha {
+ CreateHasher = secretKey => new HMACSHA512(secretKey),
+ GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA512,
+ BaseHashAlgorithm = new SHA512Managed(),
+ },
+ new HmacSha {
+ CreateHasher = secretKey => new HMACSHA384(secretKey),
+ GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA384,
+ BaseHashAlgorithm = new SHA384Managed(),
+ },
+ new HmacSha {
+ CreateHasher = secretKey => new HMACSHA256(secretKey),
+ GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA256,
+ BaseHashAlgorithm = new SHA256Managed(),
+ },
+ new HmacSha {
+ CreateHasher = secretKey => new HMACSHA1(secretKey),
+ GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA1,
+ BaseHashAlgorithm = new SHA1Managed(),
+ },
+ };
+
+ public static HmacShaAssociation Create(Protocol protocol, string associationType,
+ string handle, byte[] secret, TimeSpan totalLifeLength) {
+ foreach (HmacSha shaType in HmacShaAssociationTypes) {
+ if (String.Equals(shaType.GetAssociationType(protocol), associationType, StringComparison.Ordinal)) {
+ return new HmacShaAssociation(shaType, handle, secret, totalLifeLength);
+ }
+ }
+ throw new ArgumentOutOfRangeException("associationType");
+ }
+
+ public static HmacShaAssociation Create(int secretLength,
+ string handle, byte[] secret, TimeSpan totalLifeLength) {
+ foreach (HmacSha shaType in HmacShaAssociationTypes) {
+ if (shaType.SecretLength == secretLength) {
+ return new HmacShaAssociation(shaType, handle, secret, totalLifeLength);
+ }
+ }
+ throw new ArgumentOutOfRangeException("secretLength");
+ }
+
+ /// <summary>
+ /// Returns the length of the shared secret (in bytes).
+ /// </summary>
+ public static int GetSecretLength(Protocol protocol, string associationType) {
+ foreach (HmacSha shaType in HmacShaAssociationTypes) {
+ if (String.Equals(shaType.GetAssociationType(protocol), associationType, StringComparison.Ordinal)) {
+ return shaType.SecretLength;
+ }
+ }
+ throw new ArgumentOutOfRangeException("associationType");
+ }
+
+ /// <summary>
+ /// Looks for the longest hash length for a given protocol for which we have an association,
+ /// and perhaps a matching Diffie-Hellman session type.
+ /// </summary>
+ /// <param name="protocol">The OpenID version that dictates which associations are available.</param>
+ /// <param name="minimumHashSizeInBits">The minimum required hash length given security settings.</param>
+ /// <param name="maximumHashSizeInBits">The maximum hash length to even attempt. Useful for the RP side where we support SHA512 but most OPs do not -- why waste time trying?</param>
+ /// <param name="requireMatchingDHSessionType">True for HTTP associations, False for HTTPS associations.</param>
+ /// <param name="associationType">The resulting association type's well known protocol name. (i.e. HMAC-SHA256)</param>
+ /// <param name="sessionType">The resulting session type's well known protocol name, if a matching one is available. (i.e. DH-SHA256)</param>
+ internal static bool TryFindBestAssociation(Protocol protocol,
+ int? minimumHashSizeInBits, int? maximumHashSizeInBits, bool requireMatchingDHSessionType,
+ out string associationType, out string sessionType) {
+ if (protocol == null) throw new ArgumentNullException("protocol");
+ associationType = null;
+ sessionType = null;
+
+ // We assume this enumeration is in decreasing bit length order.
+ foreach (HmacSha sha in HmacShaAssociationTypes) {
+ int hashSizeInBits = sha.SecretLength * 8;
+ if (maximumHashSizeInBits.HasValue && hashSizeInBits > maximumHashSizeInBits.Value)
+ continue;
+ if (minimumHashSizeInBits.HasValue && hashSizeInBits < minimumHashSizeInBits.Value)
+ break;
+ sessionType = DiffieHellmanUtil.GetNameForSize(protocol, hashSizeInBits);
+ if (requireMatchingDHSessionType && sessionType == null)
+ continue;
+ associationType = sha.GetAssociationType(protocol);
+ if (associationType == null) {
+ continue;
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ internal static bool IsDHSessionCompatible(Protocol protocol, string associationType, string sessionType) {
+ // Under HTTPS, no DH encryption is required regardless of association type.
+ if (string.Equals(sessionType, protocol.Args.SessionType.NoEncryption, StringComparison.Ordinal)) {
+ return true;
+ }
+ // When there _is_ a DH session, it must match in hash length with the association type.
+ foreach (HmacSha sha in HmacShaAssociationTypes) {
+ if (string.Equals(associationType, sha.GetAssociationType(protocol), StringComparison.Ordinal)) {
+ int hashSizeInBits = sha.SecretLength * 8;
+ string matchingSessionName = DiffieHellmanUtil.GetNameForSize(protocol, hashSizeInBits);
+ if (string.Equals(sessionType, matchingSessionName, StringComparison.Ordinal)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ HmacShaAssociation(HmacSha typeIdentity, string handle, byte[] secret, TimeSpan totalLifeLength)
+ : base(handle, secret, totalLifeLength, DateTime.UtcNow) {
+ if (typeIdentity == null) throw new ArgumentNullException("typeIdentity");
+
+ Debug.Assert(secret.Length == typeIdentity.SecretLength);
+ this.typeIdentity = typeIdentity;
+ }
+
+ HmacSha typeIdentity;
+
+ internal override string GetAssociationType(Protocol protocol) {
+ return typeIdentity.GetAssociationType(protocol);
+ }
+
+ protected override HashAlgorithm CreateHasher() {
+ return typeIdentity.CreateHasher(SecretKey);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/DotNetOpenId/IEncodable.cs b/src/DotNetOpenId/IEncodable.cs
index ef5ab60..18a0ba2 100644
--- a/src/DotNetOpenId/IEncodable.cs
+++ b/src/DotNetOpenId/IEncodable.cs
@@ -27,7 +27,11 @@ namespace DotNetOpenId {
internal interface IEncodable {
EncodingType EncodingType { get; }
IDictionary<string, string> EncodedFields { get; }
+ /// <summary>
+ /// The URL that the user agent should be redirected to
+ /// in the case of <see cref="DotNetOpenId.EncodingType.IndirectMessage"/>.
+ /// Does not apply to <see cref="DotNetOpenId.EncodingType.DirectResponse"/>.
+ /// </summary>
Uri RedirectUrl { get; }
- Protocol Protocol { get; }
}
}
diff --git a/src/DotNetOpenId/IResponse.cs b/src/DotNetOpenId/IResponse.cs
index e023383..1c40401 100644
--- a/src/DotNetOpenId/IResponse.cs
+++ b/src/DotNetOpenId/IResponse.cs
@@ -1,12 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-using System.Net;
-using System.Collections.Specialized;
+using System.Net;
namespace DotNetOpenId {
/// <summary>
- /// Represents a Provider's response to an OpenId authentication request.
+ /// Represents an indirect message passed between Relying Party and Provider.
/// </summary>
public interface IResponse {
/// <summary>
diff --git a/src/DotNetOpenId/Identifier.cs b/src/DotNetOpenId/Identifier.cs
index 7618a9a..c14b57c 100644
--- a/src/DotNetOpenId/Identifier.cs
+++ b/src/DotNetOpenId/Identifier.cs
@@ -9,8 +9,29 @@ namespace DotNetOpenId {
/// <summary>
/// An Identifier is either a "http" or "https" URI, or an XRI.
/// </summary>
+ [Serializable]
public abstract class Identifier {
/// <summary>
+ /// Constructs an <see cref="Identifier"/>.
+ /// </summary>
+ /// <param name="isDiscoverySecureEndToEnd">
+ /// Whether the derived class is prepared to guarantee end-to-end discovery
+ /// and initial redirect for authentication is performed using SSL.
+ /// </param>
+ protected Identifier(bool isDiscoverySecureEndToEnd) {
+ IsDiscoverySecureEndToEnd = isDiscoverySecureEndToEnd;
+ }
+
+ /// <summary>
+ /// Whether this Identifier will ensure SSL is used throughout the discovery phase
+ /// and initial redirect of authentication.
+ /// </summary>
+ /// <remarks>
+ /// If this is False, a value of True may be obtained by calling <see cref="TryRequireSsl"/>.
+ /// </remarks>
+ protected internal bool IsDiscoverySecureEndToEnd { get; private set; }
+
+ /// <summary>
/// Converts the string representation of an Identifier to its strong type.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates"), SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads")]
@@ -49,6 +70,23 @@ namespace DotNetOpenId {
}
}
/// <summary>
+ /// Attempts to parse a string for an OpenId Identifier.
+ /// </summary>
+ /// <param name="value">The string to be parsed.</param>
+ /// <param name="result">The parsed Identifier form.</param>
+ /// <returns>
+ /// True if the operation was successful. False if the string was not a valid OpenId Identifier.
+ /// </returns>
+ public static bool TryParse(string value, out Identifier result) {
+ if (IsValid(value)) {
+ result = Parse(value);
+ return true;
+ } else {
+ result = null;
+ return false;
+ }
+ }
+ /// <summary>
/// Gets whether a given string represents a valid Identifier format.
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
@@ -61,7 +99,7 @@ namespace DotNetOpenId {
/// <returns>
/// An initialized structure containing the discovered provider endpoint information.
/// </returns>
- internal abstract ServiceEndpoint Discover();
+ internal abstract IEnumerable<ServiceEndpoint> Discover();
/// <summary>
/// Tests equality between two <see cref="Identifier"/>s.
@@ -91,5 +129,28 @@ namespace DotNetOpenId {
Debug.Fail("This should be overridden in every derived class.");
return base.GetHashCode();
}
+
+ /// <summary>
+ /// Returns an <see cref="Identifier"/> that has no URI fragment.
+ /// Quietly returns the original <see cref="Identifier"/> if it is not
+ /// a <see cref="UriIdentifier"/> or no fragment exists.
+ /// </summary>
+ internal abstract Identifier TrimFragment();
+
+ /// <summary>
+ /// Converts a given identifier to its secure equivalent.
+ /// UriIdentifiers originally created with an implied HTTP scheme change to HTTPS.
+ /// Discovery is made to require SSL for the entire resolution process.
+ /// </summary>
+ /// <param name="secureIdentifier">
+ /// The newly created secure identifier.
+ /// If the conversion fails, <paramref name="secureIdentifier"/> retains
+ /// <i>this</i> identifiers identity, but will never discover any endpoints.
+ /// </param>
+ /// <returns>
+ /// True if the secure conversion was successful.
+ /// False if the Identifier was originally created with an explicit HTTP scheme.
+ /// </returns>
+ internal abstract bool TryRequireSsl(out Identifier secureIdentifier);
}
}
diff --git a/src/DotNetOpenId/KeyValueFormEncoding.cs b/src/DotNetOpenId/KeyValueFormEncoding.cs
index 52e31ce..c944563 100644
--- a/src/DotNetOpenId/KeyValueFormEncoding.cs
+++ b/src/DotNetOpenId/KeyValueFormEncoding.cs
@@ -122,13 +122,13 @@ namespace DotNetOpenId {
string[] parts = line.Split(new[] { ':' }, 2);
if (parts.Length != 2) {
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
- Strings.InvalidKeyValueFormCharacterMissing, ':'));
+ Strings.InvalidKeyValueFormCharacterMissing, ':', line_num, line));
}
if (ConformanceLevel > KeyValueFormConformanceLevel.Loose) {
if (char.IsWhiteSpace(parts[0], parts[0].Length-1) ||
char.IsWhiteSpace(parts[1], 0)) {
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
- Strings.InvalidCharacterInKeyValueFormInput, ' '));
+ Strings.InvalidCharacterInKeyValueFormInput, ' ', line_num, line));
}
}
if (ConformanceLevel < KeyValueFormConformanceLevel.OpenId20) {
@@ -143,7 +143,7 @@ namespace DotNetOpenId {
reader.BaseStream.Seek(-1, SeekOrigin.End);
if (reader.BaseStream.ReadByte() != '\n') {
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
- Strings.InvalidKeyValueFormCharacterMissing, "\\n"));
+ Strings.InvalidKeyValueFormCharacterMissing, "\\n", line_num, line));
}
}
return dict;
diff --git a/src/DotNetOpenId/Logger.cs b/src/DotNetOpenId/Logger.cs
new file mode 100644
index 0000000..14d61ac
--- /dev/null
+++ b/src/DotNetOpenId/Logger.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Globalization;
+using DotNetOpenId.Loggers;
+
+namespace DotNetOpenId {
+ /// <summary>
+ /// A general logger for the entire DotNetOpenId library.
+ /// </summary>
+ /// <remarks>
+ /// Because this logger is intended for use with non-localized strings, the
+ /// overloads that take <see cref="CultureInfo"/> have been removed, and
+ /// <see cref="CultureInfo.InvariantCulture"/> is used implicitly.
+ /// </remarks>
+ static class Logger {
+ static ILog facade = initializeFacade();
+
+ static ILog initializeFacade() {
+ ILog result = Log4NetLogger.Initialize() ?? TraceLogger.Initialize() ?? NoOpLogger.Initialize();
+ result.Info(Util.DotNetOpenIdVersion);
+ return result;
+ }
+
+ #region ILog Members
+ // Although this static class doesn't literally implement the ILog interface,
+ // we implement (mostly) all the same methods in a static way.
+
+ public static void Debug(object message) {
+ facade.Debug(message);
+ }
+
+ public static void Debug(object message, Exception exception) {
+ facade.Debug(message, exception);
+ }
+
+ public static void DebugFormat(string format, params object[] args) {
+ facade.DebugFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public static void DebugFormat(string format, object arg0) {
+ facade.DebugFormat(format, arg0);
+ }
+
+ public static void DebugFormat(string format, object arg0, object arg1) {
+ facade.DebugFormat(format, arg0, arg1);
+ }
+
+ public static void DebugFormat(string format, object arg0, object arg1, object arg2) {
+ facade.DebugFormat(format, arg0, arg1, arg2);
+ }
+
+ /*
+ public static void DebugFormat(IFormatProvider provider, string format, params object[] args) {
+ facade.DebugFormat(provider, format, args);
+ }
+ */
+
+ public static void Info(object message) {
+ facade.Info(message);
+ }
+
+ public static void Info(object message, Exception exception) {
+ facade.Info(message, exception);
+ }
+
+ public static void InfoFormat(string format, params object[] args) {
+ facade.InfoFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public static void InfoFormat(string format, object arg0) {
+ facade.InfoFormat(format, arg0);
+ }
+
+ public static void InfoFormat(string format, object arg0, object arg1) {
+ facade.InfoFormat(format, arg0, arg1);
+ }
+
+ public static void InfoFormat(string format, object arg0, object arg1, object arg2) {
+ facade.InfoFormat(format, arg0, arg1, arg2);
+ }
+
+ /*
+ public static void InfoFormat(IFormatProvider provider, string format, params object[] args) {
+ facade.InfoFormat(provider, format, args);
+ }
+ */
+
+ public static void Warn(object message) {
+ facade.Warn(message);
+ }
+
+ public static void Warn(object message, Exception exception) {
+ facade.Warn(message, exception);
+ }
+
+ public static void WarnFormat(string format, params object[] args) {
+ facade.WarnFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public static void WarnFormat(string format, object arg0) {
+ facade.WarnFormat(format, arg0);
+ }
+
+ public static void WarnFormat(string format, object arg0, object arg1) {
+ facade.WarnFormat(format, arg0, arg1);
+ }
+
+ public static void WarnFormat(string format, object arg0, object arg1, object arg2) {
+ facade.WarnFormat(format, arg0, arg1, arg2);
+ }
+
+ /*
+ public static void WarnFormat(IFormatProvider provider, string format, params object[] args) {
+ facade.WarnFormat(provider, format, args);
+ }
+ */
+
+ public static void Error(object message) {
+ facade.Error(message);
+ }
+
+ public static void Error(object message, Exception exception) {
+ facade.Error(message, exception);
+ }
+
+ public static void ErrorFormat(string format, params object[] args) {
+ facade.ErrorFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public static void ErrorFormat(string format, object arg0) {
+ facade.ErrorFormat(format, arg0);
+ }
+
+ public static void ErrorFormat(string format, object arg0, object arg1) {
+ facade.ErrorFormat(format, arg0, arg1);
+ }
+
+ public static void ErrorFormat(string format, object arg0, object arg1, object arg2) {
+ facade.ErrorFormat(format, arg0, arg1, arg2);
+ }
+
+ /*
+ public static void ErrorFormat(IFormatProvider provider, string format, params object[] args) {
+ facade.ErrorFormat(provider, format, args);
+ }
+ */
+
+ public static void Fatal(object message) {
+ facade.Fatal(message);
+ }
+
+ public static void Fatal(object message, Exception exception) {
+ facade.Fatal(message, exception);
+ }
+
+ public static void FatalFormat(string format, params object[] args) {
+ facade.FatalFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public static void FatalFormat(string format, object arg0) {
+ facade.FatalFormat(format, arg0);
+ }
+
+ public static void FatalFormat(string format, object arg0, object arg1) {
+ facade.FatalFormat(format, arg0, arg1);
+ }
+
+ public static void FatalFormat(string format, object arg0, object arg1, object arg2) {
+ facade.FatalFormat(format, arg0, arg1, arg2);
+ }
+
+ /*
+ public static void FatalFormat(IFormatProvider provider, string format, params object[] args) {
+ facade.FatalFormat(provider, format, args);
+ }
+ */
+
+ public static bool IsDebugEnabled {
+ get { return facade.IsDebugEnabled; }
+ }
+
+ public static bool IsInfoEnabled {
+ get { return facade.IsInfoEnabled; }
+ }
+
+ public static bool IsWarnEnabled {
+ get { return facade.IsWarnEnabled; }
+ }
+
+ public static bool IsErrorEnabled {
+ get { return facade.IsErrorEnabled; }
+ }
+
+ public static bool IsFatalEnabled {
+ get { return facade.IsFatalEnabled; }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenId/Loggers/ILog.cs b/src/DotNetOpenId/Loggers/ILog.cs
new file mode 100644
index 0000000..2c37a7f
--- /dev/null
+++ b/src/DotNetOpenId/Loggers/ILog.cs
@@ -0,0 +1,964 @@
+#region Copyright & License
+//
+// Copyright 2001-2006 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+using System;
+using System.Reflection;
+using log4net;
+using log4net.Core;
+
+// This interface is designed to look like log4net's ILog interface.
+// We have this as a facade in front of it to avoid crashing if the
+// hosting web site chooses not to deploy log4net.dll along with
+// dotnetopenid.dll.
+
+namespace DotNetOpenId.Loggers
+{
+ /// <summary>
+ /// The ILog interface is use by application to log messages into
+ /// the log4net framework.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// Use the <see cref="LogManager"/> to obtain logger instances
+ /// that implement this interface. The <see cref="LogManager.GetLogger(Assembly,Type)"/>
+ /// static method is used to get logger instances.
+ /// </para>
+ /// <para>
+ /// This class contains methods for logging at different levels and also
+ /// has properties for determining if those logging levels are
+ /// enabled in the current configuration.
+ /// </para>
+ /// <para>
+ /// This interface can be implemented in different ways. This documentation
+ /// specifies reasonable behavior that a caller can expect from the actual
+ /// implementation, however different implementations reserve the right to
+ /// do things differently.
+ /// </para>
+ /// </remarks>
+ /// <example>Simple example of logging messages
+ /// <code lang="C#">
+ /// ILog log = LogManager.GetLogger("application-log");
+ ///
+ /// log.Info("Application Start");
+ /// log.Debug("This is a debug message");
+ ///
+ /// if (log.IsDebugEnabled)
+ /// {
+ /// log.Debug("This is another debug message");
+ /// }
+ /// </code>
+ /// </example>
+ /// <seealso cref="LogManager"/>
+ /// <seealso cref="LogManager.GetLogger(Assembly, Type)"/>
+ /// <author>Nicko Cadell</author>
+ /// <author>Gert Driesen</author>
+ interface ILog
+ {
+ /// <overloads>Log a message object with the <see cref="Level.Debug"/> level.</overloads>
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <remarks>
+ /// <para>
+ /// This method first checks if this logger is <c>DEBUG</c>
+ /// enabled by comparing the level of this logger with the
+ /// <see cref="Level.Debug"/> level. If this logger is
+ /// <c>DEBUG</c> enabled, then it converts the message object
+ /// (passed as parameter) to a string by invoking the appropriate
+ /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then
+ /// proceeds to call all the registered appenders in this logger
+ /// and also higher in the hierarchy depending on the value of
+ /// the additivity flag.
+ /// </para>
+ /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/>
+ /// to this method will print the name of the <see cref="Exception"/>
+ /// but no stack trace. To print a stack trace use the
+ /// <see cref="Debug(object,Exception)"/> form instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object,Exception)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void Debug(object message);
+
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Debug"/> level including
+ /// the stack trace of the <see cref="Exception"/> passed
+ /// as a parameter.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <param name="exception">The exception to log, including its stack trace.</param>
+ /// <remarks>
+ /// <para>
+ /// See the <see cref="Debug(object)"/> form for more detailed information.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void Debug(object message, Exception exception);
+
+ /// <overloads>Log a formatted string with the <see cref="Level.Debug"/> level.</overloads>
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void DebugFormat(string format, params object[] args);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void DebugFormat(string format, object arg0);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void DebugFormat(string format, object arg0, object arg1);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <param name="arg2">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void DebugFormat(string format, object arg0, object arg1, object arg2);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <param name="provider">An <see cref="IFormatProvider"/> that supplies culture-specific formatting information</param>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Debug(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="IsDebugEnabled"/>
+ void DebugFormat(IFormatProvider provider, string format, params object[] args);
+
+ /// <overloads>Log a message object with the <see cref="Level.Info"/> level.</overloads>
+ /// <summary>
+ /// Logs a message object with the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This method first checks if this logger is <c>INFO</c>
+ /// enabled by comparing the level of this logger with the
+ /// <see cref="Level.Info"/> level. If this logger is
+ /// <c>INFO</c> enabled, then it converts the message object
+ /// (passed as parameter) to a string by invoking the appropriate
+ /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then
+ /// proceeds to call all the registered appenders in this logger
+ /// and also higher in the hierarchy depending on the value of the
+ /// additivity flag.
+ /// </para>
+ /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/>
+ /// to this method will print the name of the <see cref="Exception"/>
+ /// but no stack trace. To print a stack trace use the
+ /// <see cref="Info(object,Exception)"/> form instead.
+ /// </para>
+ /// </remarks>
+ /// <param name="message">The message object to log.</param>
+ /// <seealso cref="Info(object,Exception)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void Info(object message);
+
+ /// <summary>
+ /// Logs a message object with the <c>INFO</c> level including
+ /// the stack trace of the <see cref="Exception"/> passed
+ /// as a parameter.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <param name="exception">The exception to log, including its stack trace.</param>
+ /// <remarks>
+ /// <para>
+ /// See the <see cref="Info(object)"/> form for more detailed information.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Info(object)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void Info(object message, Exception exception);
+
+ /// <overloads>Log a formatted message string with the <see cref="Level.Info"/> level.</overloads>
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Info(object,Exception)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void InfoFormat(string format, params object[] args);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Info(object)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void InfoFormat(string format, object arg0);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Info(object)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void InfoFormat(string format, object arg0, object arg1);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <param name="arg2">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Info(object)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void InfoFormat(string format, object arg0, object arg1, object arg2);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <param name="provider">An <see cref="IFormatProvider"/> that supplies culture-specific formatting information</param>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Info(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Info(object,Exception)"/>
+ /// <seealso cref="IsInfoEnabled"/>
+ void InfoFormat(IFormatProvider provider, string format, params object[] args);
+
+ /// <overloads>Log a message object with the <see cref="Level.Warn"/> level.</overloads>
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This method first checks if this logger is <c>WARN</c>
+ /// enabled by comparing the level of this logger with the
+ /// <see cref="Level.Warn"/> level. If this logger is
+ /// <c>WARN</c> enabled, then it converts the message object
+ /// (passed as parameter) to a string by invoking the appropriate
+ /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then
+ /// proceeds to call all the registered appenders in this logger
+ /// and also higher in the hierarchy depending on the value of the
+ /// additivity flag.
+ /// </para>
+ /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/>
+ /// to this method will print the name of the <see cref="Exception"/>
+ /// but no stack trace. To print a stack trace use the
+ /// <see cref="Warn(object,Exception)"/> form instead.
+ /// </para>
+ /// </remarks>
+ /// <param name="message">The message object to log.</param>
+ /// <seealso cref="Warn(object,Exception)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void Warn(object message);
+
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Warn"/> level including
+ /// the stack trace of the <see cref="Exception"/> passed
+ /// as a parameter.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <param name="exception">The exception to log, including its stack trace.</param>
+ /// <remarks>
+ /// <para>
+ /// See the <see cref="Warn(object)"/> form for more detailed information.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Warn(object)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void Warn(object message, Exception exception);
+
+ /// <overloads>Log a formatted message string with the <see cref="Level.Warn"/> level.</overloads>
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Warn(object,Exception)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void WarnFormat(string format, params object[] args);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Warn(object)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void WarnFormat(string format, object arg0);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Warn(object)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void WarnFormat(string format, object arg0, object arg1);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <param name="arg2">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Warn(object)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void WarnFormat(string format, object arg0, object arg1, object arg2);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <param name="provider">An <see cref="IFormatProvider"/> that supplies culture-specific formatting information</param>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Warn(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Warn(object,Exception)"/>
+ /// <seealso cref="IsWarnEnabled"/>
+ void WarnFormat(IFormatProvider provider, string format, params object[] args);
+
+ /// <overloads>Log a message object with the <see cref="Level.Error"/> level.</overloads>
+ /// <summary>
+ /// Logs a message object with the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <remarks>
+ /// <para>
+ /// This method first checks if this logger is <c>ERROR</c>
+ /// enabled by comparing the level of this logger with the
+ /// <see cref="Level.Error"/> level. If this logger is
+ /// <c>ERROR</c> enabled, then it converts the message object
+ /// (passed as parameter) to a string by invoking the appropriate
+ /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then
+ /// proceeds to call all the registered appenders in this logger
+ /// and also higher in the hierarchy depending on the value of the
+ /// additivity flag.
+ /// </para>
+ /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/>
+ /// to this method will print the name of the <see cref="Exception"/>
+ /// but no stack trace. To print a stack trace use the
+ /// <see cref="Error(object,Exception)"/> form instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object,Exception)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void Error(object message);
+
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Error"/> level including
+ /// the stack trace of the <see cref="Exception"/> passed
+ /// as a parameter.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <param name="exception">The exception to log, including its stack trace.</param>
+ /// <remarks>
+ /// <para>
+ /// See the <see cref="Error(object)"/> form for more detailed information.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void Error(object message, Exception exception);
+
+ /// <overloads>Log a formatted message string with the <see cref="Level.Error"/> level.</overloads>
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object,Exception)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void ErrorFormat(string format, params object[] args);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void ErrorFormat(string format, object arg0);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void ErrorFormat(string format, object arg0, object arg1);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <param name="arg2">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void ErrorFormat(string format, object arg0, object arg1, object arg2);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <param name="provider">An <see cref="IFormatProvider"/> that supplies culture-specific formatting information</param>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Error(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Error(object,Exception)"/>
+ /// <seealso cref="IsErrorEnabled"/>
+ void ErrorFormat(IFormatProvider provider, string format, params object[] args);
+
+ /// <overloads>Log a message object with the <see cref="Level.Fatal"/> level.</overloads>
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <remarks>
+ /// <para>
+ /// This method first checks if this logger is <c>FATAL</c>
+ /// enabled by comparing the level of this logger with the
+ /// <see cref="Level.Fatal"/> level. If this logger is
+ /// <c>FATAL</c> enabled, then it converts the message object
+ /// (passed as parameter) to a string by invoking the appropriate
+ /// <see cref="log4net.ObjectRenderer.IObjectRenderer"/>. It then
+ /// proceeds to call all the registered appenders in this logger
+ /// and also higher in the hierarchy depending on the value of the
+ /// additivity flag.
+ /// </para>
+ /// <para><b>WARNING</b> Note that passing an <see cref="Exception"/>
+ /// to this method will print the name of the <see cref="Exception"/>
+ /// but no stack trace. To print a stack trace use the
+ /// <see cref="Fatal(object,Exception)"/> form instead.
+ /// </para>
+ /// </remarks>
+ /// <param name="message">The message object to log.</param>
+ /// <seealso cref="Fatal(object,Exception)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void Fatal(object message);
+
+ /// <summary>
+ /// Log a message object with the <see cref="Level.Fatal"/> level including
+ /// the stack trace of the <see cref="Exception"/> passed
+ /// as a parameter.
+ /// </summary>
+ /// <param name="message">The message object to log.</param>
+ /// <param name="exception">The exception to log, including its stack trace.</param>
+ /// <remarks>
+ /// <para>
+ /// See the <see cref="Fatal(object)"/> form for more detailed information.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Fatal(object)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void Fatal(object message, Exception exception);
+
+ /// <overloads>Log a formatted message string with the <see cref="Level.Fatal"/> level.</overloads>
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Fatal(object,Exception)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void FatalFormat(string format, params object[] args);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Fatal(object)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void FatalFormat(string format, object arg0);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Fatal(object)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void FatalFormat(string format, object arg0, object arg1);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="arg0">An Object to format</param>
+ /// <param name="arg1">An Object to format</param>
+ /// <param name="arg2">An Object to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object,Exception)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Fatal(object)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void FatalFormat(string format, object arg0, object arg1, object arg2);
+
+ /// <summary>
+ /// Logs a formatted message string with the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <param name="provider">An <see cref="IFormatProvider"/> that supplies culture-specific formatting information</param>
+ /// <param name="format">A String containing zero or more format items</param>
+ /// <param name="args">An Object array containing zero or more objects to format</param>
+ /// <remarks>
+ /// <para>
+ /// The message is formatted using the <c>String.Format</c> method. See
+ /// <see cref="String.Format(string, object[])"/> for details of the syntax of the format string and the behavior
+ /// of the formatting.
+ /// </para>
+ /// <para>
+ /// This method does not take an <see cref="Exception"/> object to include in the
+ /// log event. To pass an <see cref="Exception"/> use one of the <see cref="Fatal(object)"/>
+ /// methods instead.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Fatal(object,Exception)"/>
+ /// <seealso cref="IsFatalEnabled"/>
+ void FatalFormat(IFormatProvider provider, string format, params object[] args);
+
+ /// <summary>
+ /// Checks if this logger is enabled for the <see cref="Level.Debug"/> level.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this logger is enabled for <see cref="Level.Debug"/> events, <c>false</c> otherwise.
+ /// </value>
+ /// <remarks>
+ /// <para>
+ /// This function is intended to lessen the computational cost of
+ /// disabled log debug statements.
+ /// </para>
+ /// <para> For some ILog interface <c>log</c>, when you write:</para>
+ /// <code lang="C#">
+ /// log.Debug("This is entry number: " + i );
+ /// </code>
+ /// <para>
+ /// You incur the cost constructing the message, string construction and concatenation in
+ /// this case, regardless of whether the message is logged or not.
+ /// </para>
+ /// <para>
+ /// If you are worried about speed (who isn't), then you should write:
+ /// </para>
+ /// <code lang="C#">
+ /// if (log.IsDebugEnabled)
+ /// {
+ /// log.Debug("This is entry number: " + i );
+ /// }
+ /// </code>
+ /// <para>
+ /// This way you will not incur the cost of parameter
+ /// construction if debugging is disabled for <c>log</c>. On
+ /// the other hand, if the <c>log</c> is debug enabled, you
+ /// will incur the cost of evaluating whether the logger is debug
+ /// enabled twice. Once in <see cref="IsDebugEnabled"/> and once in
+ /// the <see cref="Debug(object)"/>. This is an insignificant overhead
+ /// since evaluating a logger takes about 1% of the time it
+ /// takes to actually log. This is the preferred style of logging.
+ /// </para>
+ /// <para>Alternatively if your logger is available statically then the is debug
+ /// enabled state can be stored in a static variable like this:
+ /// </para>
+ /// <code lang="C#">
+ /// private static readonly bool isDebugEnabled = log.IsDebugEnabled;
+ /// </code>
+ /// <para>
+ /// Then when you come to log you can write:
+ /// </para>
+ /// <code lang="C#">
+ /// if (isDebugEnabled)
+ /// {
+ /// log.Debug("This is entry number: " + i );
+ /// }
+ /// </code>
+ /// <para>
+ /// This way the debug enabled state is only queried once
+ /// when the class is loaded. Using a <c>private static readonly</c>
+ /// variable is the most efficient because it is a run time constant
+ /// and can be heavily optimized by the JIT compiler.
+ /// </para>
+ /// <para>
+ /// Of course if you use a static readonly variable to
+ /// hold the enabled state of the logger then you cannot
+ /// change the enabled state at runtime to vary the logging
+ /// that is produced. You have to decide if you need absolute
+ /// speed or runtime flexibility.
+ /// </para>
+ /// </remarks>
+ /// <seealso cref="Debug(object)"/>
+ /// <seealso cref="DebugFormat(IFormatProvider, string, object[])"/>
+ bool IsDebugEnabled { get; }
+
+ /// <summary>
+ /// Checks if this logger is enabled for the <see cref="Level.Info"/> level.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this logger is enabled for <see cref="Level.Info"/> events, <c>false</c> otherwise.
+ /// </value>
+ /// <remarks>
+ /// For more information see <see cref="ILog.IsDebugEnabled"/>.
+ /// </remarks>
+ /// <seealso cref="Info(object)"/>
+ /// <seealso cref="InfoFormat(IFormatProvider, string, object[])"/>
+ /// <seealso cref="ILog.IsDebugEnabled"/>
+ bool IsInfoEnabled { get; }
+
+ /// <summary>
+ /// Checks if this logger is enabled for the <see cref="Level.Warn"/> level.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this logger is enabled for <see cref="Level.Warn"/> events, <c>false</c> otherwise.
+ /// </value>
+ /// <remarks>
+ /// For more information see <see cref="ILog.IsDebugEnabled"/>.
+ /// </remarks>
+ /// <seealso cref="Warn(object)"/>
+ /// <seealso cref="WarnFormat(IFormatProvider, string, object[])"/>
+ /// <seealso cref="ILog.IsDebugEnabled"/>
+ bool IsWarnEnabled { get; }
+
+ /// <summary>
+ /// Checks if this logger is enabled for the <see cref="Level.Error"/> level.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this logger is enabled for <see cref="Level.Error"/> events, <c>false</c> otherwise.
+ /// </value>
+ /// <remarks>
+ /// For more information see <see cref="ILog.IsDebugEnabled"/>.
+ /// </remarks>
+ /// <seealso cref="Error(object)"/>
+ /// <seealso cref="ErrorFormat(IFormatProvider, string, object[])"/>
+ /// <seealso cref="ILog.IsDebugEnabled"/>
+ bool IsErrorEnabled { get; }
+
+ /// <summary>
+ /// Checks if this logger is enabled for the <see cref="Level.Fatal"/> level.
+ /// </summary>
+ /// <value>
+ /// <c>true</c> if this logger is enabled for <see cref="Level.Fatal"/> events, <c>false</c> otherwise.
+ /// </value>
+ /// <remarks>
+ /// For more information see <see cref="ILog.IsDebugEnabled"/>.
+ /// </remarks>
+ /// <seealso cref="Fatal(object)"/>
+ /// <seealso cref="FatalFormat(IFormatProvider, string, object[])"/>
+ /// <seealso cref="ILog.IsDebugEnabled"/>
+ bool IsFatalEnabled { get; }
+ }
+}
diff --git a/src/DotNetOpenId/Loggers/Log4NetLogger.cs b/src/DotNetOpenId/Loggers/Log4NetLogger.cs
new file mode 100644
index 0000000..6d9a120
--- /dev/null
+++ b/src/DotNetOpenId/Loggers/Log4NetLogger.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+
+namespace DotNetOpenId.Loggers {
+ class Log4NetLogger : ILog {
+ private log4net.ILog log4netLogger;
+
+ private Log4NetLogger(log4net.ILog logger) {
+ log4netLogger = logger;
+ }
+
+ /// <summary>
+ /// Returns a new log4net logger if it exists, or returns null if the assembly cannot be found.
+ /// </summary>
+ internal static ILog Initialize() {
+ return isLog4NetPresent ? createLogger() : null;
+ }
+
+ static bool isLog4NetPresent {
+ get {
+ try {
+ Assembly.Load("log4net");
+ return true;
+ } catch (FileNotFoundException) {
+ return false;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Creates the log4net.LogManager. Call ONLY once log4net.dll is known to be present.
+ /// </summary>
+ static ILog createLogger() {
+ try {
+ return new Log4NetLogger(log4net.LogManager.GetLogger("DotNetOpenId"));
+ } catch (FileLoadException) { // wrong log4net.dll version
+ return null;
+ }
+ }
+
+ #region ILog Members
+
+ public void Debug(object message) {
+ log4netLogger.Debug(message);
+ }
+
+ public void Debug(object message, Exception exception) {
+ log4netLogger.Debug(message, exception);
+ }
+
+ public void DebugFormat(string format, params object[] args) {
+ log4netLogger.DebugFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public void DebugFormat(string format, object arg0) {
+ log4netLogger.DebugFormat(format, arg0);
+ }
+
+ public void DebugFormat(string format, object arg0, object arg1) {
+ log4netLogger.DebugFormat(format, arg0, arg1);
+ }
+
+ public void DebugFormat(string format, object arg0, object arg1, object arg2) {
+ log4netLogger.DebugFormat(format, arg0, arg1, arg2);
+ }
+
+ public void DebugFormat(IFormatProvider provider, string format, params object[] args) {
+ log4netLogger.DebugFormat(provider, format, args);
+ }
+
+ public void Info(object message) {
+ log4netLogger.Info(message);
+ }
+
+ public void Info(object message, Exception exception) {
+ log4netLogger.Info(message, exception);
+ }
+
+ public void InfoFormat(string format, params object[] args) {
+ log4netLogger.InfoFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public void InfoFormat(string format, object arg0) {
+ log4netLogger.InfoFormat(format, arg0);
+ }
+
+ public void InfoFormat(string format, object arg0, object arg1) {
+ log4netLogger.InfoFormat(format, arg0, arg1);
+ }
+
+ public void InfoFormat(string format, object arg0, object arg1, object arg2) {
+ log4netLogger.InfoFormat(format, arg0, arg1, arg2);
+ }
+
+ public void InfoFormat(IFormatProvider provider, string format, params object[] args) {
+ log4netLogger.InfoFormat(provider, format, args);
+ }
+
+ public void Warn(object message) {
+ log4netLogger.Warn(message);
+ }
+
+ public void Warn(object message, Exception exception) {
+ log4netLogger.Warn(message, exception);
+ }
+
+ public void WarnFormat(string format, params object[] args) {
+ log4netLogger.WarnFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public void WarnFormat(string format, object arg0) {
+ log4netLogger.WarnFormat(format, arg0);
+ }
+
+ public void WarnFormat(string format, object arg0, object arg1) {
+ log4netLogger.WarnFormat(format, arg0, arg1);
+ }
+
+ public void WarnFormat(string format, object arg0, object arg1, object arg2) {
+ log4netLogger.WarnFormat(format, arg0, arg1, arg2);
+ }
+
+ public void WarnFormat(IFormatProvider provider, string format, params object[] args) {
+ log4netLogger.WarnFormat(provider, format, args);
+ }
+
+ public void Error(object message) {
+ log4netLogger.Error(message);
+ }
+
+ public void Error(object message, Exception exception) {
+ log4netLogger.Error(message, exception);
+ }
+
+ public void ErrorFormat(string format, params object[] args) {
+ log4netLogger.ErrorFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public void ErrorFormat(string format, object arg0) {
+ log4netLogger.ErrorFormat(format, arg0);
+ }
+
+ public void ErrorFormat(string format, object arg0, object arg1) {
+ log4netLogger.ErrorFormat(format, arg0, arg1);
+ }
+
+ public void ErrorFormat(string format, object arg0, object arg1, object arg2) {
+ log4netLogger.ErrorFormat(format, arg0, arg1, arg2);
+ }
+
+ public void ErrorFormat(IFormatProvider provider, string format, params object[] args) {
+ log4netLogger.ErrorFormat(provider, format, args);
+ }
+
+ public void Fatal(object message) {
+ log4netLogger.Fatal(message);
+ }
+
+ public void Fatal(object message, Exception exception) {
+ log4netLogger.Fatal(message, exception);
+ }
+
+ public void FatalFormat(string format, params object[] args) {
+ log4netLogger.FatalFormat(CultureInfo.InvariantCulture, format, args);
+ }
+
+ public void FatalFormat(string format, object arg0) {
+ log4netLogger.FatalFormat(format, arg0);
+ }
+
+ public void FatalFormat(string format, object arg0, object arg1) {
+ log4netLogger.FatalFormat(format, arg0, arg1);
+ }
+
+ public void FatalFormat(string format, object arg0, object arg1, object arg2) {
+ log4netLogger.FatalFormat(format, arg0, arg1, arg2);
+ }
+
+ public void FatalFormat(IFormatProvider provider, string format, params object[] args) {
+ log4netLogger.FatalFormat(provider, format, args);
+ }
+
+ public bool IsDebugEnabled {
+ get { return log4netLogger.IsDebugEnabled; }
+ }
+
+ public bool IsInfoEnabled {
+ get { return log4netLogger.IsInfoEnabled; }
+ }
+
+ public bool IsWarnEnabled {
+ get { return log4netLogger.IsWarnEnabled; }
+ }
+
+ public bool IsErrorEnabled {
+ get { return log4netLogger.IsErrorEnabled; }
+ }
+
+ public bool IsFatalEnabled {
+ get { return log4netLogger.IsFatalEnabled; }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenId/Loggers/NoOpLogger.cs b/src/DotNetOpenId/Loggers/NoOpLogger.cs
new file mode 100644
index 0000000..c38078c
--- /dev/null
+++ b/src/DotNetOpenId/Loggers/NoOpLogger.cs
@@ -0,0 +1,177 @@
+using System;
+
+namespace DotNetOpenId.Loggers {
+ class NoOpLogger : ILog {
+
+ /// <summary>
+ /// Returns a new logger that does nothing when invoked.
+ /// </summary>
+ internal static ILog Initialize() {
+ return new NoOpLogger();
+ }
+
+ #region ILog Members
+
+ public void Debug(object message) {
+ return;
+ }
+
+ public void Debug(object message, Exception exception) {
+ return;
+ }
+
+ public void DebugFormat(string format, params object[] args) {
+ return;
+ }
+
+ public void DebugFormat(string format, object arg0) {
+ return;
+ }
+
+ public void DebugFormat(string format, object arg0, object arg1) {
+ return;
+ }
+
+ public void DebugFormat(string format, object arg0, object arg1, object arg2) {
+ return;
+ }
+
+ public void DebugFormat(IFormatProvider provider, string format, params object[] args) {
+ return;
+ }
+
+ public void Info(object message) {
+ return;
+ }
+
+ public void Info(object message, Exception exception) {
+ return;
+ }
+
+ public void InfoFormat(string format, params object[] args) {
+ return;
+ }
+
+ public void InfoFormat(string format, object arg0) {
+ return;
+ }
+
+ public void InfoFormat(string format, object arg0, object arg1) {
+ return;
+ }
+
+ public void InfoFormat(string format, object arg0, object arg1, object arg2) {
+ return;
+ }
+
+ public void InfoFormat(IFormatProvider provider, string format, params object[] args) {
+ return;
+ }
+
+ public void Warn(object message) {
+ return;
+ }
+
+ public void Warn(object message, Exception exception) {
+ return;
+ }
+
+ public void WarnFormat(string format, params object[] args) {
+ return;
+ }
+
+ public void WarnFormat(string format, object arg0) {
+ return;
+ }
+
+ public void WarnFormat(string format, object arg0, object arg1) {
+ return;
+ }
+
+ public void WarnFormat(string format, object arg0, object arg1, object arg2) {
+ return;
+ }
+
+ public void WarnFormat(IFormatProvider provider, string format, params object[] args) {
+ return;
+ }
+
+ public void Error(object message) {
+ return;
+ }
+
+ public void Error(object message, Exception exception) {
+ return;
+ }
+
+ public void ErrorFormat(string format, params object[] args) {
+ return;
+ }
+
+ public void ErrorFormat(string format, object arg0) {
+ return;
+ }
+
+ public void ErrorFormat(string format, object arg0, object arg1) {
+ return;
+ }
+
+ public void ErrorFormat(string format, object arg0, object arg1, object arg2) {
+ return;
+ }
+
+ public void ErrorFormat(IFormatProvider provider, string format, params object[] args) {
+ return;
+ }
+
+ public void Fatal(object message) {
+ return;
+ }
+
+ public void Fatal(object message, Exception exception) {
+ return;
+ }
+
+ public void FatalFormat(string format, params object[] args) {
+ return;
+ }
+
+ public void FatalFormat(string format, object arg0) {
+ return;
+ }
+
+ public void FatalFormat(string format, object arg0, object arg1) {
+ return;
+ }
+
+ public void FatalFormat(string format, object arg0, object arg1, object arg2) {
+ return;
+ }
+
+ public void FatalFormat(IFormatProvider provider, string format, params object[] args) {
+ return;
+ }
+
+ public bool IsDebugEnabled {
+ get { return false; }
+ }
+
+ public bool IsInfoEnabled {
+ get { return false; }
+ }
+
+ public bool IsWarnEnabled {
+ get { return false; }
+ }
+
+ public bool IsErrorEnabled {
+ get { return false; }
+ }
+
+ public bool IsFatalEnabled {
+ get { return false; }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenId/Loggers/TraceLogger.cs b/src/DotNetOpenId/Loggers/TraceLogger.cs
new file mode 100644
index 0000000..bdf5fd3
--- /dev/null
+++ b/src/DotNetOpenId/Loggers/TraceLogger.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Diagnostics;
+using System.Security;
+using System.Security.Permissions;
+
+namespace DotNetOpenId.Loggers {
+ class TraceLogger : ILog {
+ TraceSwitch traceSwitch = new TraceSwitch("OpenID", "OpenID Trace Switch");
+
+ /// <summary>
+ /// Returns a new logger that uses the <see cref="System.Diagnostics.Trace"/> class
+ /// if sufficient CAS permissions are granted to use it, otherwise returns false.
+ /// </summary>
+ internal static ILog Initialize() {
+ return isSufficientPermissionGranted ? new TraceLogger() : null;
+ }
+
+ static bool isSufficientPermissionGranted {
+ get {
+ PermissionSet permissions = new PermissionSet(PermissionState.None);
+ permissions.AddPermission(new KeyContainerPermission(PermissionState.Unrestricted));
+ permissions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess));
+ permissions.AddPermission(new RegistryPermission(PermissionState.Unrestricted));
+ permissions.AddPermission(new SecurityPermission(SecurityPermissionFlag.ControlEvidence | SecurityPermissionFlag.UnmanagedCode | SecurityPermissionFlag.ControlThread));
+ var file = new FileIOPermission(PermissionState.None);
+ file.AllFiles = FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read;
+ permissions.AddPermission(file);
+ try {
+ permissions.Demand();
+ return true;
+ } catch (SecurityException) {
+ return false;
+ }
+ }
+ }
+
+ #region ILog Members
+
+ public void Debug(object message) {
+ Trace.TraceInformation(message.ToString());
+ }
+
+ public void Debug(object message, Exception exception) {
+ Trace.TraceInformation(message + ": " + exception.ToString());
+ }
+
+ public void DebugFormat(string format, params object[] args) {
+ Trace.TraceInformation(format, args);
+ }
+
+ public void DebugFormat(string format, object arg0) {
+ Trace.TraceInformation(format, arg0);
+ }
+
+ public void DebugFormat(string format, object arg0, object arg1) {
+ Trace.TraceInformation(format, arg0, arg1);
+ }
+
+ public void DebugFormat(string format, object arg0, object arg1, object arg2) {
+ Trace.TraceInformation(format, arg0, arg1, arg2);
+ }
+
+ public void DebugFormat(IFormatProvider provider, string format, params object[] args) {
+ Trace.TraceInformation(format, args);
+ }
+
+ public void Info(object message) {
+ Trace.TraceInformation(message.ToString());
+ }
+
+ public void Info(object message, Exception exception) {
+ Trace.TraceInformation(message + ": " + exception.ToString());
+ }
+
+ public void InfoFormat(string format, params object[] args) {
+ Trace.TraceInformation(format, args);
+ }
+
+ public void InfoFormat(string format, object arg0) {
+ Trace.TraceInformation(format, arg0);
+ }
+
+ public void InfoFormat(string format, object arg0, object arg1) {
+ Trace.TraceInformation(format, arg0, arg1);
+ }
+
+ public void InfoFormat(string format, object arg0, object arg1, object arg2) {
+ Trace.TraceInformation(format, arg0, arg1, arg2);
+ }
+
+ public void InfoFormat(IFormatProvider provider, string format, params object[] args) {
+ Trace.TraceInformation(format, args);
+ }
+
+ public void Warn(object message) {
+ Trace.TraceWarning(message.ToString());
+ }
+
+ public void Warn(object message, Exception exception) {
+ Trace.TraceWarning(message + ": " + exception.ToString());
+ }
+
+ public void WarnFormat(string format, params object[] args) {
+ Trace.TraceWarning(format, args);
+ }
+
+ public void WarnFormat(string format, object arg0) {
+ Trace.TraceWarning(format, arg0);
+ }
+
+ public void WarnFormat(string format, object arg0, object arg1) {
+ Trace.TraceWarning(format, arg0, arg1);
+ }
+
+ public void WarnFormat(string format, object arg0, object arg1, object arg2) {
+ Trace.TraceWarning(format, arg0, arg1, arg2);
+ }
+
+ public void WarnFormat(IFormatProvider provider, string format, params object[] args) {
+ Trace.TraceWarning(format, args);
+ }
+
+ public void Error(object message) {
+ Trace.TraceError(message.ToString());
+ }
+
+ public void Error(object message, Exception exception) {
+ Trace.TraceError(message + ": " + exception.ToString());
+ }
+
+ public void ErrorFormat(string format, params object[] args) {
+ Trace.TraceError(format, args);
+ }
+
+ public void ErrorFormat(string format, object arg0) {
+ Trace.TraceError(format, arg0);
+ }
+
+ public void ErrorFormat(string format, object arg0, object arg1) {
+ Trace.TraceError(format, arg0, arg1);
+ }
+
+ public void ErrorFormat(string format, object arg0, object arg1, object arg2) {
+ Trace.TraceError(format, arg0, arg1, arg2);
+ }
+
+ public void ErrorFormat(IFormatProvider provider, string format, params object[] args) {
+ Trace.TraceError(format, args);
+ }
+
+ public void Fatal(object message) {
+ Trace.TraceError(message.ToString());
+ }
+
+ public void Fatal(object message, Exception exception) {
+ Trace.TraceError(message + ": " + exception.ToString());
+ }
+
+ public void FatalFormat(string format, params object[] args) {
+ Trace.TraceError(format, args);
+ }
+
+ public void FatalFormat(string format, object arg0) {
+ Trace.TraceError(format, arg0);
+ }
+
+ public void FatalFormat(string format, object arg0, object arg1) {
+ Trace.TraceError(format, arg0, arg1);
+ }
+
+ public void FatalFormat(string format, object arg0, object arg1, object arg2) {
+ Trace.TraceError(format, arg0, arg1, arg2);
+ }
+
+ public void FatalFormat(IFormatProvider provider, string format, params object[] args) {
+ Trace.TraceError(format, args);
+ }
+
+ public bool IsDebugEnabled {
+ get { return traceSwitch.TraceVerbose; }
+ }
+
+ public bool IsInfoEnabled {
+ get { return traceSwitch.TraceInfo; }
+ }
+
+ public bool IsWarnEnabled {
+ get { return traceSwitch.TraceWarning; }
+ }
+
+ public bool IsErrorEnabled {
+ get { return traceSwitch.TraceError; }
+ }
+
+ public bool IsFatalEnabled {
+ get { return traceSwitch.TraceError; }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/DotNetOpenId/MessageEncoder.cs b/src/DotNetOpenId/MessageEncoder.cs
index 3d5f840..2de2c0e 100644
--- a/src/DotNetOpenId/MessageEncoder.cs
+++ b/src/DotNetOpenId/MessageEncoder.cs
@@ -1,106 +1,129 @@
-using System;
-using System.Collections.Specialized;
-using System.Text;
-using System.Net;
-using System.Diagnostics;
-using DotNetOpenId.Provider;
-using System.IO;
-using System.Web;
-
-namespace DotNetOpenId {
- /// <summary>
- /// Encodes <see cref="IEncodable"/> messages into <see cref="Response"/> instances
- /// that can be interpreted by the host web site.
- /// </summary>
- internal class MessageEncoder {
- /// <summary>
- /// The maximum allowable size for a 301 Redirect response before we send
- /// a 200 OK response with a scripted form POST with the parameters instead
- /// in order to ensure successfully sending a large payload to another server
- /// that might have a maximum allowable size restriction on its GET request.
- /// </summary>
- internal static int GetToPostThreshold = 2 * 1024; // 2KB, recommended by OpenID group
- // We are intentionally using " instead of the html single quote ' below because
- // the HtmlEncode'd values that we inject will only escape the double quote, so
- // only the double-quote used around these values is safe.
- const string FormPostFormat = @"
-<html>
-<body onload=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; document.getElementById('openid_message').submit()"">
-<form id=""openid_message"" action=""{0}"" method=""post"" accept-charset=""UTF-8"" enctype=""application/x-www-form-urlencoded"" onSubmit=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; return true;"">
-{1}
- <input id=""submit_button"" type=""submit"" value=""Continue"" />
-</form>
-</body>
-</html>
-";
- /// <summary>
- /// Encodes messages into <see cref="Response"/> instances.
- /// </summary>
- public virtual Response Encode(IEncodable message) {
- EncodingType encode_as = message.EncodingType;
- Response wr;
-
- WebHeaderCollection headers = new WebHeaderCollection();
- switch (encode_as) {
- case EncodingType.DirectResponse:
- HttpStatusCode code = (message is Exception) ?
- HttpStatusCode.BadRequest : HttpStatusCode.OK;
- wr = new Response(code, headers, ProtocolMessages.KeyValueForm.GetBytes(message.EncodedFields), message);
- break;
- case EncodingType.IndirectMessage:
- // TODO: either redirect or do a form POST depending on payload size.
- Debug.Assert(message.RedirectUrl != null);
- if (getSizeOfPayload(message) <= GetToPostThreshold)
- wr = Create301RedirectResponse(message);
- else
- wr = CreateFormPostResponse(message);
- break;
- default:
- if (TraceUtil.Switch.TraceError) {
- Trace.TraceError("Cannot encode response: {0}", message);
- }
- wr = new Response(HttpStatusCode.BadRequest, headers, new byte[0], message);
- break;
- }
- return wr;
- }
-
- static int getSizeOfPayload(IEncodable message) {
- Debug.Assert(message != null);
- int size = 0;
- foreach (var field in message.EncodedFields) {
- size += field.Key.Length;
- size += field.Value.Length;
- }
- return size;
- }
- protected virtual Response Create301RedirectResponse(IEncodable message) {
- WebHeaderCollection headers = new WebHeaderCollection();
- UriBuilder builder = new UriBuilder(message.RedirectUrl);
- UriUtil.AppendQueryArgs(builder, message.EncodedFields);
- headers.Add(HttpResponseHeader.Location, builder.Uri.AbsoluteUri);
- return new Response(HttpStatusCode.Redirect, headers, new byte[0], message);
- }
- protected virtual Response CreateFormPostResponse(IEncodable message) {
- WebHeaderCollection headers = new WebHeaderCollection();
- MemoryStream body = new MemoryStream();
- StreamWriter bodyWriter = new StreamWriter(body);
- StringBuilder hiddenFields = new StringBuilder();
- foreach (var field in message.EncodedFields) {
- hiddenFields.AppendFormat("\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n",
- HttpUtility.HtmlEncode(field.Key), HttpUtility.HtmlEncode(field.Value));
- }
- bodyWriter.WriteLine(FormPostFormat,
- HttpUtility.HtmlEncode(message.RedirectUrl.AbsoluteUri), hiddenFields);
- bodyWriter.Flush();
- return new Response(HttpStatusCode.OK, headers, body.ToArray(), message);
- }
- }
-
- internal class EncodeEventArgs : EventArgs {
- public EncodeEventArgs(IEncodable encodable) {
- Message = encodable;
- }
- public IEncodable Message { get; private set; }
- }
-}
+using System;
+using System.Collections.Specialized;
+using System.Text;
+using System.Net;
+using System.Diagnostics;
+using DotNetOpenId.Provider;
+using System.IO;
+using System.Web;
+
+namespace DotNetOpenId {
+ /// <summary>
+ /// Encodes <see cref="IEncodable"/> messages into <see cref="Response"/> instances
+ /// that can be interpreted by the host web site.
+ /// </summary>
+ internal class MessageEncoder {
+ /// <summary>
+ /// The HTTP Content-Type to use in Key-Value Form responses.
+ /// </summary>
+ /// <remarks>
+ /// OpenID 2.0 section 5.1.2 says this SHOULD be text/plain. But this value
+ /// does not prevent free hosters like GoDaddy from tacking on their ads
+ /// to the end of the direct response, corrupting the data. So we deviate
+ /// from the spec a bit here to improve the story for free Providers.
+ /// </remarks>
+ const string KeyValueFormContentType = "application/x-openid-kvf";
+ /// <summary>
+ /// The maximum allowable size for a 301 Redirect response before we send
+ /// a 200 OK response with a scripted form POST with the parameters instead
+ /// in order to ensure successfully sending a large payload to another server
+ /// that might have a maximum allowable size restriction on its GET request.
+ /// </summary>
+ internal static int GetToPostThreshold = 2 * 1024; // 2KB, per OpenID group and http://support.microsoft.com/kb/q208427/
+ // We are intentionally using " instead of the html single quote ' below because
+ // the HtmlEncode'd values that we inject will only escape the double quote, so
+ // only the double-quote used around these values is safe.
+ const string FormPostFormat = @"
+<html>
+<body onload=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; document.getElementById('openid_message').submit()"">
+<form id=""openid_message"" action=""{0}"" method=""post"" accept-charset=""UTF-8"" enctype=""application/x-www-form-urlencoded"" onSubmit=""var btn = document.getElementById('submit_button'); btn.disabled = true; btn.value = 'Login in progress'; return true;"">
+{1}
+ <input id=""submit_button"" type=""submit"" value=""Continue"" />
+</form>
+</body>
+</html>
+";
+ /// <summary>
+ /// Encodes messages into <see cref="Response"/> instances.
+ /// </summary>
+ public virtual Response Encode(IEncodable message) {
+ if (message == null) throw new ArgumentNullException("message");
+
+ EncodingType encode_as = message.EncodingType;
+ Response wr;
+
+ WebHeaderCollection headers = new WebHeaderCollection();
+ switch (encode_as) {
+ case EncodingType.DirectResponse:
+ Logger.DebugFormat("Sending direct message response:{0}{1}",
+ Environment.NewLine, Util.ToString(message.EncodedFields));
+ HttpStatusCode code = (message is Exception) ?
+ HttpStatusCode.BadRequest : HttpStatusCode.OK;
+ // Key-Value Encoding is how response bodies are sent.
+ // Setting the content-type to something other than text/html or text/plain
+ // prevents free hosted sites like GoDaddy's from automatically appending
+ // the <script/> at the end that adds a banner, and invalidating our response.
+ headers.Add(HttpResponseHeader.ContentType, KeyValueFormContentType);
+ wr = new Response(code, headers, ProtocolMessages.KeyValueForm.GetBytes(message.EncodedFields), message);
+ break;
+ case EncodingType.IndirectMessage:
+ Logger.DebugFormat("Sending indirect message:{0}{1}",
+ Environment.NewLine, Util.ToString(message.EncodedFields));
+ // TODO: either redirect or do a form POST depending on payload size.
+ Debug.Assert(message.RedirectUrl != null);
+ if (getSizeOfPayload(message) <= GetToPostThreshold)
+ wr = Create301RedirectResponse(message);
+ else
+ wr = CreateFormPostResponse(message);
+ break;
+ default:
+ Logger.ErrorFormat("Cannot encode response: {0}", message);
+ wr = new Response(HttpStatusCode.BadRequest, headers, new byte[0], message);
+ break;
+ }
+ return wr;
+ }
+
+ /// <summary>
+ /// Gets the length of the URL that would be used for a simple redirect to carry
+ /// this indirect message to its recipient.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <returns>The number of characters in the redirect URL.</returns>
+ static int getSizeOfPayload(IEncodable message) {
+ Debug.Assert(message != null);
+ UriBuilder redirect = new UriBuilder(message.RedirectUrl);
+ UriUtil.AppendQueryArgs(redirect, message.EncodedFields);
+ return redirect.Uri.AbsoluteUri.Length;
+ }
+ protected virtual Response Create301RedirectResponse(IEncodable message) {
+ WebHeaderCollection headers = new WebHeaderCollection();
+ UriBuilder builder = new UriBuilder(message.RedirectUrl);
+ UriUtil.AppendQueryArgs(builder, message.EncodedFields);
+ headers.Add(HttpResponseHeader.Location, builder.Uri.AbsoluteUri);
+ Logger.DebugFormat("Redirecting to {0}", builder.Uri.AbsoluteUri);
+ return new Response(HttpStatusCode.Redirect, headers, new byte[0], message);
+ }
+ protected virtual Response CreateFormPostResponse(IEncodable message) {
+ WebHeaderCollection headers = new WebHeaderCollection();
+ MemoryStream body = new MemoryStream();
+ StreamWriter bodyWriter = new StreamWriter(body);
+ StringBuilder hiddenFields = new StringBuilder();
+ foreach (var field in message.EncodedFields) {
+ hiddenFields.AppendFormat("\t<input type=\"hidden\" name=\"{0}\" value=\"{1}\" />\r\n",
+ HttpUtility.HtmlEncode(field.Key), HttpUtility.HtmlEncode(field.Value));
+ }
+ bodyWriter.WriteLine(FormPostFormat,
+ HttpUtility.HtmlEncode(message.RedirectUrl.AbsoluteUri), hiddenFields);
+ bodyWriter.Flush();
+ return new Response(HttpStatusCode.OK, headers, body.ToArray(), message);
+ }
+ }
+
+ internal class EncodeEventArgs : EventArgs {
+ public EncodeEventArgs(IEncodable encodable) {
+ Message = encodable;
+ }
+ public IEncodable Message { get; private set; }
+ }
+}
diff --git a/src/DotNetOpenId/NoDiscoveryIdentifier.cs b/src/DotNetOpenId/NoDiscoveryIdentifier.cs
new file mode 100644
index 0000000..4b47abc
--- /dev/null
+++ b/src/DotNetOpenId/NoDiscoveryIdentifier.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using DotNetOpenId.RelyingParty;
+
+namespace DotNetOpenId {
+ /// <summary>
+ /// Wraps an existing Identifier and prevents it from performing discovery.
+ /// </summary>
+ class NoDiscoveryIdentifier : Identifier {
+ Identifier wrappedIdentifier ;
+ internal NoDiscoveryIdentifier(Identifier wrappedIdentifier, bool claimSsl)
+ : base(claimSsl) {
+ if (wrappedIdentifier == null) throw new ArgumentNullException("wrappedIdentifier");
+
+ this.wrappedIdentifier = wrappedIdentifier;
+ }
+
+ internal override IEnumerable<ServiceEndpoint> Discover() {
+ return new ServiceEndpoint[0];
+ }
+
+ internal override Identifier TrimFragment() {
+ return new NoDiscoveryIdentifier(wrappedIdentifier.TrimFragment(), IsDiscoverySecureEndToEnd);
+ }
+
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ return wrappedIdentifier.TryRequireSsl(out secureIdentifier);
+ }
+
+ public override string ToString() {
+ return wrappedIdentifier.ToString();
+ }
+
+ public override bool Equals(object obj) {
+ return wrappedIdentifier.Equals(obj);
+ }
+
+ public override int GetHashCode() {
+ return wrappedIdentifier.GetHashCode();
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Nonce.cs b/src/DotNetOpenId/Nonce.cs
index b86b4e8..3876fde 100644
--- a/src/DotNetOpenId/Nonce.cs
+++ b/src/DotNetOpenId/Nonce.cs
@@ -95,7 +95,8 @@ namespace DotNetOpenId {
internal void Consume(INonceStore store) {
if (IsExpired)
- throw new OpenIdException(Strings.ExpiredNonce);
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.ExpiredNonce, ExpirationDate, DateTime.UtcNow));
// We could store unused nonces and remove them as they are used, or
// we could store used nonces and check that they do not previously exist.
diff --git a/src/DotNetOpenId/OpenIdException.cs b/src/DotNetOpenId/OpenIdException.cs
index b523cff..f526d66 100644
--- a/src/DotNetOpenId/OpenIdException.cs
+++ b/src/DotNetOpenId/OpenIdException.cs
@@ -20,7 +20,6 @@ namespace DotNetOpenId {
/// </summary>
public Identifier Identifier { get; private set; }
internal Protocol Protocol = Protocol.Default;
- Protocol IEncodable.Protocol { get { return this.Protocol; } }
internal IDictionary<string, string> ExtraArgsToReturn;
internal OpenIdException(string message, Identifier identifier, IDictionary<string, string> query, Exception innerException)
@@ -28,6 +27,12 @@ namespace DotNetOpenId {
this.Query = query;
Identifier = identifier;
if (query != null) Protocol = Protocol.Detect(query);
+
+ if (query != null) {
+ Logger.ErrorFormat("OpenIdException: {0}{1}{2}", message, Environment.NewLine, Util.ToString(query));
+ } else {
+ Logger.ErrorFormat("OpenIdException: {0}", message);
+ }
}
internal OpenIdException(string message, Identifier identifier, IDictionary<string, string> query)
: this(message, identifier, query, null) {
@@ -79,33 +84,40 @@ namespace DotNetOpenId {
#region IEncodable Members
- EncodingType IEncodable.EncodingType {
+ /// <summary>
+ /// Gets whether this exception was generated on an OP as the result of processing a message
+ /// that came directly from the RP.
+ /// </summary>
+ /// <remarks>
+ /// This is useful because it allows us to determine what kind of error reporting we'll send
+ /// in the HTTP response.
+ /// </remarks>
+ private bool IsDirectMessage {
get {
Debug.Assert(Query != null, "An OpenId exception should always be provided with the query if it is to be encoded for transmittal to the RP.");
- if (HasReturnTo)
- return EncodingType.IndirectMessage;
if (Query != null) {
string mode = Util.GetOptionalArg(Query, Protocol.openid.mode);
- if (mode != null)
- if (mode != Protocol.Args.Mode.checkid_setup &&
- mode != Protocol.Args.Mode.checkid_immediate)
- return EncodingType.DirectResponse;
+ if (mode != null) {
+ return mode == Protocol.Args.Mode.associate ||
+ mode == Protocol.Args.Mode.check_authentication;
+ }
}
- // Notes from the original port
- //# According to the OpenID spec as of this writing, we are
- //# probably supposed to switch on request type here (GET
- //# versus POST) to figure out if we're supposed to print
- //# machine-readable or human-readable content at this
- //# point. GET/POST seems like a pretty lousy way of making
- //# the distinction though, as it's just as possible that
- //# the user agent could have mistakenly been directed to
- //# post to the server URL.
+ // Unable to figure it out, so we'll default to indirect message.
+ return false;
+ }
+ }
+
+ EncodingType IEncodable.EncodingType {
+ get {
+ if (IsDirectMessage)
+ return EncodingType.DirectResponse;
- //# Basically, if your request was so broken that you didn't
- //# manage to include an openid.mode, I'm not going to worry
- //# too much about returning you something you can't parse.
+ if (HasReturnTo)
+ return EncodingType.IndirectMessage;
+
+ Debug.Fail("Somehow we cannot tell whether this is a direct message or indirect message. Did we construct an exception without a Query parameter?");
return EncodingType.None;
}
}
@@ -118,8 +130,13 @@ namespace DotNetOpenId {
public IDictionary<string, string> EncodedFields {
get {
var q = new Dictionary<string, string>();
- q.Add(Protocol.openid.mode, Protocol.Args.Mode.error);
- q.Add(Protocol.openid.error, Message);
+ if (IsDirectMessage) {
+ q.Add(Protocol.openidnp.mode, Protocol.Args.Mode.error);
+ q.Add(Protocol.openidnp.error, Message);
+ } else {
+ q.Add(Protocol.openid.mode, Protocol.Args.Mode.error);
+ q.Add(Protocol.openid.error, Message);
+ }
if (ExtraArgsToReturn != null) {
foreach (var pair in ExtraArgsToReturn) {
q.Add(pair.Key, pair.Value);
diff --git a/src/DotNetOpenId/Properties/AssemblyInfo.cs b/src/DotNetOpenId/Properties/AssemblyInfo.cs
index 0801fdd..f25b6ae 100644
--- a/src/DotNetOpenId/Properties/AssemblyInfo.cs
+++ b/src/DotNetOpenId/Properties/AssemblyInfo.cs
@@ -4,16 +4,22 @@
// it will fail before doing much damage.
// But a partially trusted assembly's events, handled by the hosting
// web site, will also be under the partial trust restriction.
+// Also note that http://support.microsoft.com/kb/839300 states a
+// strong-name signed assembly must use AllowPartiallyTrustedCallers
+// to be called from a web page, but defining PARTIAL_TRUST below also
+// accomplishes this.
//#define PARTIAL_TRUST
+// We DON'T put an AssemblyVersionAttribute in here because it is generated in the build.
+
+using System;
+using System.Net;
using System.Reflection;
+using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Security;
using System.Security.Permissions;
-using System.Web;
-using System.Net;
-using System.Resources;
-using System;
using System.Web.UI;
[assembly: TagPrefix("DotNetOpenId", "openid")]
@@ -28,7 +34,7 @@ using System.Web.UI;
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("DotNetOpenId")]
-[assembly: AssemblyCopyright("Copyright © 2007")]
+[assembly: AssemblyCopyright("Copyright © 2008")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en-US")]
@@ -41,19 +47,11 @@ using System.Web.UI;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("7d73990c-47c0-4256-9f20-a893add9e289")]
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Revision and Build Numbers
-// by using the '*' as shown below:
-[assembly: AssemblyVersion("2.3.0.0")]
-[assembly: AssemblyFileVersion("2.3.0.0")]
-
#if StrongNameSigned
+// See comment at top of this file. We need this so that strong-naming doesn't
+// keep this assembly from being useful to shared host (medium trust) web sites.
+[assembly: AllowPartiallyTrustedCallers]
+
[assembly: InternalsVisibleTo("DotNetOpenId.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")]
#else
[assembly: InternalsVisibleTo("DotNetOpenId.Test")]
@@ -69,26 +67,19 @@ using System.Web.UI;
// Allows the consumer to call out to the web server. This is unnecessary in provider-only scenarios.
// Note: we don't use a single demand for https?://.* because the regex pattern must exactly
// match the one used by hosting providers. Listing them individually seems to be more common.
-#if !__MonoCS__
+#if !__MonoCS__
[assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"http://.*")]
[assembly: WebPermission(SecurityAction.RequestMinimum, ConnectPattern = @"https://.*")]
-#endif
+#endif
+
+#if PARTIAL_TRUST
// Allows hosting this assembly in an ASP.NET setting. Not all applications
// will host this using ASP.NET, so this is optional. Besides, we need at least
// one optional permission to activate CAS permission shrinking.
-#if PARTIAL_TRUST
[assembly: AspNetHostingPermission(SecurityAction.RequestOptional, Level = AspNetHostingPermissionLevel.Medium)]
-#endif
// The following are only required for diagnostic logging (Trace.Write, Debug.Assert, etc.).
-#if TRACE
-[assembly: KeyContainerPermission(SecurityAction.RequestMinimum, Unrestricted = true)]
-[assembly: ReflectionPermission(SecurityAction.RequestMinimum, MemberAccess = true)]
-[assembly: RegistryPermission(SecurityAction.RequestMinimum, Unrestricted = true)]
-[assembly: SecurityPermission(SecurityAction.RequestMinimum, ControlEvidence = true, UnmanagedCode = true, ControlThread = true)]
-[assembly: FileIOPermission(SecurityAction.RequestMinimum, AllFiles = FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read)]
-#else
-#if PARTIAL_TRUST
+#if TRACE || DEBUG
[assembly: KeyContainerPermission(SecurityAction.RequestOptional, Unrestricted = true)]
[assembly: ReflectionPermission(SecurityAction.RequestOptional, MemberAccess = true)]
[assembly: RegistryPermission(SecurityAction.RequestOptional, Unrestricted = true)]
diff --git a/src/DotNetOpenId/Protocol.cs b/src/DotNetOpenId/Protocol.cs
index df26e55..aacec53 100644
--- a/src/DotNetOpenId/Protocol.cs
+++ b/src/DotNetOpenId/Protocol.cs
@@ -75,9 +75,13 @@ namespace DotNetOpenId {
SessionType = new QueryArguments.SessionTypes() {
NoEncryption = "no-encryption",
DH_SHA256 = "DH-SHA256",
+ DH_SHA384 = "DH-SHA384",
+ DH_SHA512 = "DH-SHA512",
},
SignatureAlgorithm = new QueryArguments.SignatureAlgorithms() {
HMAC_SHA256 = "HMAC-SHA256",
+ HMAC_SHA384 = "HMAC-SHA384",
+ HMAC_SHA512 = "HMAC-SHA512",
},
Mode = new QueryArguments.Modes() {
setup_needed = "setup_needed",
@@ -108,18 +112,48 @@ namespace DotNetOpenId {
}
/// <summary>
/// Attempts to detect the right OpenID protocol version based on the contents
- /// of an incoming query string.
+ /// of an incoming OpenID <i>indirect</i> message or <i>direct request</i>.
/// </summary>
internal static Protocol Detect(IDictionary<string, string> Query) {
if (Query == null) throw new ArgumentNullException("Query");
return Query.ContainsKey(v20.openid.ns) ? v20 : v11;
}
+ /// <summary>
+ /// Attempts to detect the right OpenID protocol version based on the contents
+ /// of an incoming OpenID <i>direct</i> response message.
+ /// </summary>
+ internal static Protocol DetectFromDirectResponse(IDictionary<string, string> Query) {
+ if (Query == null) throw new ArgumentNullException("Query");
+ return Query.ContainsKey(v20.openidnp.ns) ? v20 : v11;
+ }
+ /// <summary>
+ /// Attemps to detect the highest OpenID protocol version supported given a set
+ /// of XRDS Service Type URIs included for some service.
+ /// </summary>
+ internal static Protocol Detect(string[] serviceTypeURIs) {
+ if (serviceTypeURIs == null) throw new ArgumentNullException("serviceTypeURIs");
+ return Util.FindBestVersion(p => p.OPIdentifierServiceTypeURI, serviceTypeURIs) ??
+ Util.FindBestVersion(p => p.ClaimedIdentifierServiceTypeURI, serviceTypeURIs) ??
+ Util.FindBestVersion(p => p.RPReturnToTypeURI, serviceTypeURIs);
+ }
/// <summary>
/// The OpenID version that this <see cref="Protocol"/> instance describes.
/// </summary>
public Version Version;
/// <summary>
+ /// Returns the <see cref="ProtocolVersion"/> enum value for the <see cref="Protocol"/> instance.
+ /// </summary>
+ public ProtocolVersion ProtocolVersion {
+ get {
+ switch (Version.Major) {
+ case 1: return ProtocolVersion.V11;
+ case 2: return ProtocolVersion.V20;
+ default: throw new ArgumentException(null); // this should never happen
+ }
+ }
+ }
+ /// <summary>
/// The namespace of OpenId 1.x elements in XRDS documents.
/// </summary>
public string XmlNamespace;
@@ -256,19 +290,43 @@ namespace DotNetOpenId {
/// <summary>
/// A preference order list of all supported session types.
/// </summary>
- public string[] All { get { return new[] { DH_SHA256, DH_SHA1, NoEncryption }; } }
- public string[] AllDiffieHellman { get { return new[] { DH_SHA256, DH_SHA1 }; } }
+ public string[] All { get { return new[] { DH_SHA512, DH_SHA384, DH_SHA256, DH_SHA1, NoEncryption }; } }
+ public string[] AllDiffieHellman { get { return new[] { DH_SHA512, DH_SHA384, DH_SHA256, DH_SHA1 }; } }
public string DH_SHA1 = "DH-SHA1";
public string DH_SHA256 = null;
+ public string DH_SHA384 = null;
+ public string DH_SHA512 = null;
public string NoEncryption = "";
+ public string Best {
+ get {
+ foreach (string algorithmName in All) {
+ if (algorithmName != null) {
+ return algorithmName;
+ }
+ }
+ throw new OpenIdException(); // really bad... we have no signing algorithms at all
+ }
+ }
}
internal class SignatureAlgorithms {
/// <summary>
/// A preference order list of signature algorithms we support.
/// </summary>
- public string[] All { get { return new [] { HMAC_SHA256, HMAC_SHA1 }; } }
+ public string[] All { get { return new[] { HMAC_SHA512, HMAC_SHA384, HMAC_SHA256, HMAC_SHA1 }; } }
public string HMAC_SHA1 = "HMAC-SHA1";
public string HMAC_SHA256 = null;
+ public string HMAC_SHA384 = null;
+ public string HMAC_SHA512 = null;
+ public string Best {
+ get {
+ foreach (string algorithmName in All) {
+ if (algorithmName != null) {
+ return algorithmName;
+ }
+ }
+ throw new OpenIdException(); // really bad... we have no signing algorithms at all
+ }
+ }
}
internal class Modes {
public string cancel = "cancel";
@@ -292,7 +350,7 @@ namespace DotNetOpenId {
/// <remarks>
/// This is used to calculate the length of time that nonces are stored.
/// This is internal until we can decide whether to leave this static, or make
- /// it an instance member, or put it inside the IConsumerAppliationStore interface.
+ /// it an instance member, or put it inside the IConsumerApplicationStore interface.
/// </remarks>
internal static TimeSpan MaximumUserAgentAuthenticationTime = TimeSpan.FromMinutes(5);
/// <summary>
diff --git a/src/DotNetOpenId/Provider/AssertionMessage.cs b/src/DotNetOpenId/Provider/AssertionMessage.cs
index 3224c61..b238595 100644
--- a/src/DotNetOpenId/Provider/AssertionMessage.cs
+++ b/src/DotNetOpenId/Provider/AssertionMessage.cs
@@ -13,7 +13,9 @@ namespace DotNetOpenId.Provider {
message.Fields[protocol.openidnp.mode] = protocol.Args.Mode.id_res;
message.Fields[protocol.openidnp.identity] = localIdentifier;
- message.Fields[protocol.openidnp.return_to] = message.RedirectUrl.AbsoluteUri;
+ // We use OriginalString for the return_to to help protect against interop
+ // problems with RPs that require an explicit port, or who knows what else.
+ message.Fields[protocol.openidnp.return_to] = message.RedirectUrl.OriginalString;
message.Signed.AddRange(new[]{
protocol.openidnp.return_to,
protocol.openidnp.identity,
@@ -34,16 +36,17 @@ namespace DotNetOpenId.Provider {
// as appropriate by the Signatory.Sign method.
}
- public static void CreateNegativeAssertion(EncodableResponse message,
- bool immediateMode, Uri setupUrl) {
+ public static void CreateNegativeAssertion(EncodableResponse message, CheckIdRequest request) {
if (message == null) throw new ArgumentNullException("message");
+ if (request == null) throw new ArgumentNullException("request");
+
Protocol protocol = message.Protocol;
- if (immediateMode) {
+ if (request.Immediate) {
if (protocol.Version.Major >= 2) {
message.Fields[protocol.openidnp.mode] = protocol.Args.Mode.setup_needed;
} else {
message.Fields[protocol.openidnp.mode] = protocol.Args.Mode.id_res;
- message.Fields[protocol.openidnp.user_setup_url] = setupUrl.AbsoluteUri;
+ message.Fields[protocol.openidnp.user_setup_url] = request.SetupUrl.AbsoluteUri;
}
} else {
message.Fields[protocol.openidnp.mode] = protocol.Args.Mode.cancel;
@@ -58,12 +61,10 @@ namespace DotNetOpenId.Provider {
if (request.IsAuthenticated.Value) {
AssertionMessage.CreatePositiveAssertion(response, request.Provider,
request.LocalIdentifier, request.ClaimedIdentifier);
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Created positive assertion for {0}.", request.ClaimedIdentifier);
+ Logger.InfoFormat("Created positive assertion for {0}.", request.ClaimedIdentifier);
} else {
- AssertionMessage.CreateNegativeAssertion(response, request.Immediate, request.SetupUrl);
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Created negative assertion for {0}.", request.ClaimedIdentifier);
+ AssertionMessage.CreateNegativeAssertion(response, request);
+ Logger.InfoFormat("Created negative assertion for {0}.", request.ClaimedIdentifier);
}
return response;
}
diff --git a/src/DotNetOpenId/Provider/AssociateRequest.cs b/src/DotNetOpenId/Provider/AssociateRequest.cs
index 669ab71..a2f6f9a 100644
--- a/src/DotNetOpenId/Provider/AssociateRequest.cs
+++ b/src/DotNetOpenId/Provider/AssociateRequest.cs
@@ -28,7 +28,7 @@ namespace DotNetOpenId.Provider {
}
/// <summary>
- /// Used to throw a carefully crafted exception that will end up getting
+ /// This method is used to throw a carefully crafted exception that will end up getting
/// encoded as a response to the RP, given hints as to what
/// assoc_type and session_type args we support.
/// </summary>
@@ -74,13 +74,12 @@ namespace DotNetOpenId.Provider {
response.Fields[pair.Key] = nvc[pair.Key];
}
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Association {0} created.", assoc.Handle);
+ Logger.InfoFormat("Association {0} created.", assoc.Handle);
return response;
}
- internal override IEncodable CreateResponse() {
+ protected override IEncodable CreateResponse() {
return Answer();
}
diff --git a/src/DotNetOpenId/Provider/CheckAuthRequest.cs b/src/DotNetOpenId/Provider/CheckAuthRequest.cs
index a6ec0ca..bda4918 100644
--- a/src/DotNetOpenId/Provider/CheckAuthRequest.cs
+++ b/src/DotNetOpenId/Provider/CheckAuthRequest.cs
@@ -24,7 +24,7 @@ namespace DotNetOpenId.Provider {
signedFields = new Dictionary<string, string>();
Debug.Assert(!signedKeyOrder.Contains(Protocol.openidnp.mode), "openid.mode must not be included in signature because it necessarily changes in checkauth requests.");
foreach (string key in signedKeyOrder) {
- signedFields.Add(key, Util.GetRequiredArg(Query, Protocol.openid.Prefix + key));
+ signedFields.Add(key, Util.GetRequiredArgAllowEmptyValue(Query, Protocol.openid.Prefix + key));
}
}
@@ -62,9 +62,7 @@ namespace DotNetOpenId.Provider {
Association assoc = Provider.Signatory.GetAssociation(invalidate_handle, AssociationRelyingPartyType.Smart);
if (assoc == null) {
- if (TraceUtil.Switch.TraceWarning) {
- Trace.TraceWarning("No matching association found. Returning invalidate_handle. ");
- }
+ Logger.Warn("No matching association found. Returning invalidate_handle. ");
response.Fields[Protocol.openidnp.invalidate_handle] = invalidate_handle;
}
}
@@ -72,7 +70,7 @@ namespace DotNetOpenId.Provider {
return response;
}
- internal override IEncodable CreateResponse() {
+ protected override IEncodable CreateResponse() {
return Answer();
}
diff --git a/src/DotNetOpenId/Provider/CheckIdRequest.cs b/src/DotNetOpenId/Provider/CheckIdRequest.cs
index e71049c..98d7428 100644
--- a/src/DotNetOpenId/Provider/CheckIdRequest.cs
+++ b/src/DotNetOpenId/Provider/CheckIdRequest.cs
@@ -62,8 +62,7 @@ namespace DotNetOpenId.Provider {
// The spec requires that the return_to URLs given in an RPs XRDS doc
// do not contain wildcards.
if (discoveredReturnToUrl.DomainWildcard) {
- if (TraceUtil.Switch.TraceWarning)
- Trace.TraceWarning("Realm {0} contained return_to URL {1} which contains a wildcard, which is not allowed.",
+ Logger.WarnFormat("Realm {0} contained return_to URL {1} which contains a wildcard, which is not allowed.",
Realm, discoveredReturnToUrl);
continue;
}
@@ -75,13 +74,11 @@ namespace DotNetOpenId.Provider {
}
}
} catch (OpenIdException ex) {
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Relying party discovery at URL {0} failed. {1}",
+ Logger.InfoFormat("Relying party discovery at URL {0} failed. {1}",
Realm, ex);
// Don't do anything else. We quietly fail at return_to verification and return false.
} catch (WebException ex) {
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Relying party discovery at URL {0} failed. {1}",
+ Logger.InfoFormat("Relying party discovery at URL {0} failed. {1}",
Realm, ex);
// Don't do anything else. We quietly fail at return_to verification and return false.
}
@@ -94,6 +91,17 @@ namespace DotNetOpenId.Provider {
/// to send back to the relying party.
/// </summary>
public bool IsDirectedIdentity { get; private set; }
+ /// <summary>
+ /// A value indicating whether the requesting Relying Party is using a delegated URL.
+ /// </summary>
+ /// <remarks>
+ /// When delegated identifiers are used, the <see cref="ClaimedIdentifier"/> should not
+ /// be changed at the Provider during authentication.
+ /// Delegation is only detectable on requests originating from OpenID 2.0 relying parties.
+ /// A relying party implementing only OpenID 1.x may use delegation and this property will
+ /// return false anyway.
+ /// </remarks>
+ public bool IsDelegatedIdentifier { get; private set; }
Identifier localIdentifier;
/// <summary>
/// The user identifier used by this particular provider.
@@ -101,17 +109,16 @@ namespace DotNetOpenId.Provider {
public Identifier LocalIdentifier {
get { return localIdentifier; }
set {
+ // Keep LocalIdentifier and ClaimedIdentifier in sync for directed identity.
if (IsDirectedIdentity) {
- // Keep LocalIdentifier and ClaimedIdentifier in sync
if (ClaimedIdentifier != null && ClaimedIdentifier != value) {
throw new InvalidOperationException(Strings.IdentifierSelectRequiresMatchingIdentifiers);
- } else {
- localIdentifier = value;
- claimedIdentifier = value;
}
- } else {
- throw new InvalidOperationException(Strings.IdentifierSelectModeOnly);
+
+ claimedIdentifier = value;
}
+
+ localIdentifier = value;
}
}
Identifier claimedIdentifier;
@@ -121,19 +128,53 @@ namespace DotNetOpenId.Provider {
public Identifier ClaimedIdentifier {
get { return claimedIdentifier; }
set {
+ // Keep LocalIdentifier and ClaimedIdentifier in sync for directed identity.
if (IsDirectedIdentity) {
- // Keep LocalIdentifier and ClaimedIdentifier in sync
if (LocalIdentifier != null && LocalIdentifier != value) {
throw new InvalidOperationException(Strings.IdentifierSelectRequiresMatchingIdentifiers);
- } else {
- claimedIdentifier = value;
- localIdentifier = value;
}
- } else {
- throw new InvalidOperationException(Strings.IdentifierSelectModeOnly);
+
+ localIdentifier = value;
+ }
+
+ if (IsDelegatedIdentifier) {
+ throw new InvalidOperationException(Strings.ClaimedIdentifierCannotBeSetOnDelegatedAuthentication);
}
+
+ claimedIdentifier = value;
}
}
+
+ /// <summary>
+ /// Adds an optional fragment (#fragment) portion to a URI ClaimedIdentifier.
+ /// Useful for identifier recycling.
+ /// </summary>
+ /// <param name="fragment">
+ /// Should not include the # prefix character as that will be added internally.
+ /// May be null or the empty string to clear a previously set fragment.
+ /// </param>
+ /// <remarks>
+ /// <para>Unlike the <see cref="ClaimedIdentifier"/> property, which can only be set if
+ /// using directed identity, this method can be called on any URI claimed identifier.</para>
+ /// <para>Because XRI claimed identifiers (the canonical IDs) are never recycled,
+ /// this method should<i>not</i> be called for XRIs.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">
+ /// Thrown when this method is called on an XRI, or on a directed identity request
+ /// before the <see cref="ClaimedIdentifier"/> property is set.</exception>
+ public void SetClaimedIdentifierFragment(string fragment) {
+ if (IsDirectedIdentity && ClaimedIdentifier == null) {
+ throw new InvalidOperationException(Strings.ClaimedIdentifierMustBeSetFirst);
+ }
+ if (ClaimedIdentifier is XriIdentifier) {
+ throw new InvalidOperationException(Strings.FragmentNotAllowedOnXRIs);
+ }
+
+ UriBuilder builder = new UriBuilder(ClaimedIdentifier);
+ builder.Fragment = fragment;
+ claimedIdentifier = builder.Uri;
+ }
+
/// <summary>
/// The URL to redirect the user agent to after the authentication attempt.
/// This must fall "under" the realm URL.
@@ -218,18 +259,28 @@ namespace DotNetOpenId.Provider {
claimedIdentifier = null;
localIdentifier = null;
}
+
+ // URL delegation is only detectable from 2.0 RPs, since openid.claimed_id isn't included from 1.0 RPs.
+ // If the openid.claimed_id is present, and if it's different than the openid.identity argument, then
+ // the RP has discovered a claimed identifier that has delegated authentication to this Provider.
+ IsDelegatedIdentifier = ClaimedIdentifier != null && ClaimedIdentifier != LocalIdentifier;
}
- internal override IEncodable CreateResponse() {
+ protected override IEncodable CreateResponse() {
Debug.Assert(IsAuthenticated.HasValue, "This should be checked internally before CreateResponse is called.");
return AssertionMessage.CreateAssertion(this);
}
/// <summary>
/// Encode this request as a URL to GET.
+ /// Only used in response to immediate auth requests from OpenID 1.x RPs.
/// </summary>
internal Uri SetupUrl {
get {
+ if (Protocol.Version.Major >= 2) {
+ Debug.Fail("This property only applicable to OpenID 1.x RPs.");
+ throw new InvalidOperationException();
+ }
Debug.Assert(Provider.Endpoint != null, "The OpenIdProvider should have guaranteed this.");
var q = new Dictionary<string, string>();
diff --git a/src/DotNetOpenId/Provider/FaultyRequest.cs b/src/DotNetOpenId/Provider/FaultyRequest.cs
index 2d38a50..c2e3b3a 100644
--- a/src/DotNetOpenId/Provider/FaultyRequest.cs
+++ b/src/DotNetOpenId/Provider/FaultyRequest.cs
@@ -18,7 +18,7 @@ namespace DotNetOpenId.Provider {
get { return true; }
}
- internal override IEncodable CreateResponse() {
+ protected override IEncodable CreateResponse() {
return Response;
}
}
diff --git a/src/DotNetOpenId/Provider/IAuthenticationRequest.cs b/src/DotNetOpenId/Provider/IAuthenticationRequest.cs
index 7e0fd1c..56b5fc3 100644
--- a/src/DotNetOpenId/Provider/IAuthenticationRequest.cs
+++ b/src/DotNetOpenId/Provider/IAuthenticationRequest.cs
@@ -10,6 +10,10 @@ namespace DotNetOpenId.Provider {
/// </summary>
public interface IAuthenticationRequest : IRequest {
/// <summary>
+ /// Gets the version of OpenID being used by the relying party that sent the request.
+ /// </summary>
+ ProtocolVersion RelyingPartyVersion { get; }
+ /// <summary>
/// Whether the consumer demands an immediate response.
/// If false, the consumer is willing to wait for the identity provider
/// to authenticate the user.
@@ -36,6 +40,17 @@ namespace DotNetOpenId.Provider {
/// </summary>
bool IsDirectedIdentity { get; }
/// <summary>
+ /// A value indicating whether the requesting Relying Party is using a delegated URL.
+ /// </summary>
+ /// <remarks>
+ /// When delegated identifiers are used, the <see cref="ClaimedIdentifier"/> should not
+ /// be changed at the Provider during authentication.
+ /// Delegation is only detectable on requests originating from OpenID 2.0 relying parties.
+ /// A relying party implementing only OpenID 1.x may use delegation and this property will
+ /// return false anyway.
+ /// </remarks>
+ bool IsDelegatedIdentifier { get; }
+ /// <summary>
/// The Local Identifier to this OpenID Provider of the user attempting
/// to authenticate. Check <see cref="IsDirectedIdentity"/> to see if
/// this value is valid.
@@ -54,14 +69,34 @@ namespace DotNetOpenId.Provider {
/// Check <see cref="IsDirectedIdentity"/> to see if this value is valid.
/// </summary>
/// <remarks>
- /// This will not be the same as this provider's local identifier for the user
+ /// <para>This property can only be set if <see cref="IsDelegatedIdentifier"/> is
+ /// false, to prevent breaking URL delegation.</para>
+ /// <para>This will not be the same as this provider's local identifier for the user
/// if the user has set up his/her own identity page that points to this
- /// provider for authentication.
- /// The provider may use this identifier for displaying to the user when
- /// asking for the user's permission to authenticate to the relying party.
+ /// provider for authentication.</para>
+ /// <para>The provider may use this identifier for displaying to the user when
+ /// asking for the user's permission to authenticate to the relying party.</para>
/// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown from the setter
+ /// if <see cref="IsDelegatedIdentifier"/> is true.</exception>
Identifier ClaimedIdentifier { get; set; }
/// <summary>
+ /// Adds an optional fragment (#fragment) portion to the ClaimedIdentifier.
+ /// Useful for identifier recycling.
+ /// </summary>
+ /// <param name="fragment">
+ /// Should not include the # prefix character as that will be added internally.
+ /// May be null or the empty string to clear a previously set fragment.
+ /// </param>
+ /// <remarks>
+ /// <para>Unlike the <see cref="ClaimedIdentifier"/> property, which can only be set if
+ /// using directed identity, this method can be called on any URI claimed identifier.</para>
+ /// <para>Because XRI claimed identifiers (the canonical IDs) are never recycled,
+ /// this method should<i>not</i> be called for XRIs.</para>
+ /// </remarks>
+ /// <exception cref="InvalidOperationException">Thrown when this method is called on an XRI.</exception>
+ void SetClaimedIdentifierFragment(string fragment);
+ /// <summary>
/// Gets/sets whether the provider has determined that the
/// <see cref="ClaimedIdentifier"/> belongs to the currently logged in user
/// and wishes to share this information with the consumer.
diff --git a/src/DotNetOpenId/Provider/IdentityEndpoint.cs b/src/DotNetOpenId/Provider/IdentityEndpoint.cs
index 18562a6..dd7e30d 100644
--- a/src/DotNetOpenId/Provider/IdentityEndpoint.cs
+++ b/src/DotNetOpenId/Provider/IdentityEndpoint.cs
@@ -1,13 +1,60 @@
using System;
-using System.Collections.Generic;
using System.ComponentModel;
-using System.Text;
-using System.Web;
using System.Web.UI;
-using System.Web.UI.WebControls;
namespace DotNetOpenId.Provider {
/// <summary>
+ /// The event arguments passed to the <see cref="IdentityEndpoint.NormalizeUri"/> event handler.
+ /// </summary>
+ public class IdentityEndpointNormalizationEventArgs : EventArgs {
+ internal IdentityEndpointNormalizationEventArgs(UriIdentifier userSuppliedIdentifier) {
+ UserSuppliedIdentifier = userSuppliedIdentifier;
+ }
+
+ /// <summary>
+ /// Gets or sets the portion of the incoming page request URI that is relevant to normalization.
+ /// </summary>
+ /// <remarks>
+ /// This identifier should be used to look up the user whose identity page is being queried.
+ /// It MAY be set in case some clever web server URL rewriting is taking place that ASP.NET
+ /// does not know about but your site does. If this is the case this property should be set
+ /// to whatever the original request URL was.
+ /// </remarks>
+ public Uri UserSuppliedIdentifier { get; set; }
+
+ /// <summary>
+ /// Gets/sets the normalized form of the user's identifier, according to the host site's policy.
+ /// </summary>
+ /// <remarks>
+ /// <para>This should be set to some constant value for an individual user.
+ /// For example, if <see cref="UserSuppliedIdentifier"/> indicates that identity page
+ /// for "BOB" is being called up, then the following things should be considered:</para>
+ /// <list>
+ /// <item>Normalize the capitalization of the URL: for example, change http://provider/BOB to
+ /// http://provider/bob.</item>
+ /// <item>Switch to HTTPS is it is offered: change http://provider/bob to https://provider/bob.</item>
+ /// <item>Strip off the query string if it is not part of the canonical identity:
+ /// https://provider/bob?timeofday=now becomes https://provider/bob</item>
+ /// <item>Ensure that any trailing slash is either present or absent consistently. For example,
+ /// change https://provider/bob/ to https://provider/bob.</item>
+ /// </list>
+ /// <para>When this property is set, the <see cref="IdentityEndpoint"/> control compares it to
+ /// the request that actually came in, and redirects the browser to use the normalized identifier
+ /// if necessary.</para>
+ /// <para>Using the normalized identifier in the request is <i>very</i> important as it
+ /// helps the user maintain a consistent identity across sites and across site visits to an individual site.
+ /// For example, without normalizing the URL, Bob might sign into a relying party site as
+ /// http://provider/bob one day and https://provider/bob the next day, and the relying party
+ /// site <i>should</i> interpret Bob as two different people because the URLs are different.
+ /// By normalizing the URL at the Provider's identity page for Bob, whichever URL Bob types in
+ /// from day-to-day gets redirected to a normalized form, so Bob is seen as the same person
+ /// all the time, which is of course what Bob wants.
+ /// </para>
+ /// </remarks>
+ public Uri NormalizedIdentifier { get; set; }
+ }
+
+ /// <summary>
/// An ASP.NET control that manages the OpenID identity advertising tags
/// of a user's Identity Page that allow a relying party web site to discover
/// how to authenticate a user.
@@ -26,6 +73,7 @@ namespace DotNetOpenId.Provider {
/// </summary>
[Category("Behavior")]
[DefaultValue(providerVersionDefault)]
+ [Description("The OpenID version supported by the provider.")]
public ProtocolVersion ProviderVersion {
get {
return ViewState[providerVersionViewStateKey] == null ?
@@ -40,6 +88,7 @@ namespace DotNetOpenId.Provider {
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings"), Bindable(true)]
[Category("Behavior")]
+ [Description("The Provider URL that processes OpenID requests.")]
public string ProviderEndpointUrl {
get { return (string)ViewState[providerEndpointUrlViewStateKey]; }
set {
@@ -54,6 +103,7 @@ namespace DotNetOpenId.Provider {
/// </summary>
[Bindable(true)]
[Category("Behavior")]
+ [Description("The user Identifier that is controlled by the Provider.")]
public string ProviderLocalIdentifier {
get { return (string)ViewState[providerLocalIdentifierViewStateKey]; }
set {
@@ -61,6 +111,25 @@ namespace DotNetOpenId.Provider {
ViewState[providerLocalIdentifierViewStateKey] = value;
}
}
+
+ const string autoNormalizeRequestViewStateKey = "AutoNormalizeRequest";
+ /// <summary>
+ /// Whether every incoming request will be checked for normalized form and redirected if it is not.
+ /// </summary>
+ /// <remarks>
+ /// <para>If set to true (and it should be), you should also handle the <see cref="NormalizeUri"/>
+ /// event and apply your own policy for normalizing the URI.</para>
+ /// If multiple <see cref="IdentityEndpoint"/> controls are on a single page (to support
+ /// multiple versions of OpenID for example) then only one of them should have this
+ /// property set to true.
+ /// </remarks>
+ [Bindable(true)]
+ [Category("Behavior")]
+ [Description("Whether every incoming request will be checked for normalized form and redirected if it is not. If set to true, consider handling the NormalizeUri event.")]
+ public bool AutoNormalizeRequest {
+ get { return (bool)(ViewState[autoNormalizeRequestViewStateKey] ?? false); }
+ set { ViewState[autoNormalizeRequestViewStateKey] = value; }
+ }
#endregion
internal Protocol Protocol {
@@ -68,6 +137,69 @@ namespace DotNetOpenId.Provider {
}
/// <summary>
+ /// Fired at each page request so the host web site can return the normalized
+ /// version of the request URI.
+ /// </summary>
+ public event EventHandler<IdentityEndpointNormalizationEventArgs> NormalizeUri;
+
+ /// <summary>
+ /// Checks the incoming request and invokes a browser redirect if the URL has not been normalized.
+ /// </summary>
+ /// <seealso cref="IdentityEndpointNormalizationEventArgs.NormalizedIdentifier"/>
+ protected virtual void OnNormalize() {
+ UriIdentifier userSuppliedIdentifier = Util.GetRequestUrlFromContext();
+ var normalizationArgs = new IdentityEndpointNormalizationEventArgs(userSuppliedIdentifier);
+
+ var normalizeUri = NormalizeUri;
+ if (normalizeUri != null) {
+ normalizeUri(this, normalizationArgs);
+ } else {
+ // Do some best-guess normalization.
+ normalizationArgs.NormalizedIdentifier = bestGuessNormalization(normalizationArgs.UserSuppliedIdentifier);
+ }
+ // If we have a normalized form, we should use it.
+ // We compare path and query with case sensitivity and host name without case sensitivity deliberately,
+ // and the fragment will be asserted or cleared by the OP during authentication.
+ if (normalizationArgs.NormalizedIdentifier != null &&
+ (!String.Equals(normalizationArgs.NormalizedIdentifier.Host, normalizationArgs.UserSuppliedIdentifier.Host, StringComparison.OrdinalIgnoreCase) ||
+ !String.Equals(normalizationArgs.NormalizedIdentifier.PathAndQuery, normalizationArgs.UserSuppliedIdentifier.PathAndQuery, StringComparison.Ordinal))) {
+ Page.Response.Redirect(normalizationArgs.NormalizedIdentifier.AbsoluteUri);
+ }
+ }
+
+ /// <summary>
+ /// Normalizes the URL by making the path and query lowercase, and trimming trailing slashes.
+ /// </summary>
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification="FxCop is probably right, but we've been lowercasing host names for normalization elsewhere in the project for a long time now.")]
+ private static Uri bestGuessNormalization(Uri uri) {
+ UriBuilder uriBuilder = new UriBuilder(uri);
+ uriBuilder.Path = uriBuilder.Path.ToLowerInvariant();
+ // Ensure no trailing slash unless it is the only element of the path.
+ if (uriBuilder.Path != "/") {
+ uriBuilder.Path = uriBuilder.Path.TrimEnd('/');
+ }
+ // We trim the ? from the start of the query when we reset it because
+ // the UriBuilder.Query setter automatically prepends one, and we don't
+ // want to double them up.
+ uriBuilder.Query = uriBuilder.Query.TrimStart('?').ToLowerInvariant();
+ return uriBuilder.Uri;
+ }
+
+ /// <summary>
+ /// Checks the incoming request and invokes a browser redirect if the URL has not been normalized.
+ /// </summary>
+ protected override void OnLoad(EventArgs e) {
+ // Perform URL normalization BEFORE calling base.OnLoad, to keep
+ // our base XrdsPublisher from over-eagerly responding with an XRDS
+ // document before we've redirected.
+ if (AutoNormalizeRequest && !Page.IsPostBack) {
+ OnNormalize();
+ }
+
+ base.OnLoad(e);
+ }
+
+ /// <summary>
/// Renders OpenID identity tags.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings")]
@@ -77,7 +209,7 @@ namespace DotNetOpenId.Provider {
writer.WriteBeginTag("link");
writer.WriteAttribute("rel", Protocol.HtmlDiscoveryProviderKey);
writer.WriteAttribute("href",
- new Uri(Page.Request.Url, Page.ResolveUrl(ProviderEndpointUrl)).AbsoluteUri);
+ new Uri(Util.GetRequestUrlFromContext(), Page.ResolveUrl(ProviderEndpointUrl)).AbsoluteUri);
writer.Write(">");
writer.WriteEndTag("link");
writer.WriteLine();
@@ -86,7 +218,7 @@ namespace DotNetOpenId.Provider {
writer.WriteBeginTag("link");
writer.WriteAttribute("rel", Protocol.HtmlDiscoveryLocalIdKey);
writer.WriteAttribute("href",
- new Uri(Page.Request.Url, Page.ResolveUrl(ProviderLocalIdentifier)).AbsoluteUri);
+ new Uri(Util.GetRequestUrlFromContext(), Page.ResolveUrl(ProviderLocalIdentifier)).AbsoluteUri);
writer.Write(">");
writer.WriteEndTag("link");
writer.WriteLine();
diff --git a/src/DotNetOpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenId/Provider/OpenIdProvider.cs
index e0b2afd..29318a7 100644
--- a/src/DotNetOpenId/Provider/OpenIdProvider.cs
+++ b/src/DotNetOpenId/Provider/OpenIdProvider.cs
@@ -1,12 +1,12 @@
using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
-using System.Text;
+using System.Configuration;
+using System.Diagnostics;
using System.Web;
using IProviderAssociationStore = DotNetOpenId.IAssociationStore<DotNetOpenId.AssociationRelyingPartyType>;
using ProviderMemoryStore = DotNetOpenId.AssociationMemoryStore<DotNetOpenId.AssociationRelyingPartyType>;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Net;
+using DotNetOpenId.Configuration;
namespace DotNetOpenId.Provider {
/// <summary>
@@ -35,6 +35,10 @@ namespace DotNetOpenId.Provider {
/// </summary>
internal Protocol Protocol { get; private set; }
+ internal static Uri DefaultProviderEndpoint { get { return getProviderEndpointFromContext(); } }
+ internal static Uri DefaultRequestUrl { get { return Util.GetRequestUrlFromContext(); } }
+ internal static NameValueCollection DefaultQuery { get { return Util.GetQueryOrFormFromContextNVC(); } }
+
/// <summary>
/// Constructs an OpenId server that uses the HttpApplication dictionary as
/// its association store and detects common settings.
@@ -43,8 +47,8 @@ namespace DotNetOpenId.Provider {
/// This method requires a current ASP.NET HttpContext.
/// </remarks>
public OpenIdProvider()
- : this(HttpApplicationStore,
- getProviderEndpointFromContext(), Util.GetRequestUrlFromContext(), Util.GetQueryFromContext()) { }
+ : this(Configuration.Store.CreateInstanceOfStore(HttpApplicationStore),
+ getProviderEndpointFromContext(), Util.GetRequestUrlFromContext(), Util.GetQueryOrFormFromContext()) { }
/// <summary>
/// Constructs an OpenId server that uses a given query and IAssociationStore.
/// </summary>
@@ -67,6 +71,7 @@ namespace DotNetOpenId.Provider {
if (providerEndpoint == null) throw new ArgumentNullException("providerEndpoint");
if (requestUrl == null) throw new ArgumentNullException("requestUrl");
if (query == null) throw new ArgumentNullException("query");
+ Settings = new ProviderSecuritySettings();
Endpoint = providerEndpoint;
RequestUrl = requestUrl;
Query = query;
@@ -83,6 +88,13 @@ namespace DotNetOpenId.Provider {
/// </remarks>
internal Uri Endpoint { get; private set; }
+ // TODO: make this property public WHEN its security settings are actually supported.
+ /// <summary>
+ /// Provides access to the adjustable security settings of this instance
+ /// of <see cref="OpenIdProvider"/>.
+ /// </summary>
+ internal ProviderSecuritySettings Settings { get; private set; }
+
bool requestProcessed;
Request request;
/// <summary>
@@ -116,8 +128,8 @@ namespace DotNetOpenId.Provider {
Protocol = Protocol.Detect(Query);
Request req = Provider.Request.CreateRequest(this);
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Received OpenID {0} request.", req.Mode);
+ Logger.InfoFormat("Received OpenID {0} request.{1}{2}", req.Mode, Environment.NewLine,
+ Util.ToString(Query));
return req;
}
@@ -150,6 +162,8 @@ namespace DotNetOpenId.Provider {
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (claimedIdentifier == null) throw new ArgumentNullException("claimedIdentifier");
if (localIdentifier == null) throw new ArgumentNullException("localIdentifier");
+
+ Logger.InfoFormat("Preparing unsolicited assertion for {0}", claimedIdentifier);
return AssertionMessage.CreateUnsolicitedAssertion(this,
relyingParty, claimedIdentifier, localIdentifier);
}
@@ -182,10 +196,21 @@ namespace DotNetOpenId.Provider {
HttpContext context = HttpContext.Current;
if (context == null)
throw new InvalidOperationException(Strings.HttpContextRequiredForThisOverload);
- UriBuilder builder = new UriBuilder(HttpContext.Current.Request.Url);
+ UriBuilder builder = new UriBuilder(Util.GetRequestUrlFromContext());
builder.Query = null;
builder.Fragment = null;
return builder.Uri;
}
+
+ /// <summary>
+ /// Gets the relevant Configuration section for this OpenIdRelyingParty.
+ /// </summary>
+ /// <remarks>
+ /// This is not a static member because depending on the context within which we are
+ /// invoked, the configuration section might be different. (location tag, for example).
+ /// </remarks>
+ internal static ProviderSection Configuration {
+ get { return ProviderSection.Configuration; }
+ }
}
}
diff --git a/src/DotNetOpenId/Provider/ProviderEndpoint.cs b/src/DotNetOpenId/Provider/ProviderEndpoint.cs
index d46d248..40d8dea 100644
--- a/src/DotNetOpenId/Provider/ProviderEndpoint.cs
+++ b/src/DotNetOpenId/Provider/ProviderEndpoint.cs
@@ -5,6 +5,7 @@ using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
+using IProviderAssociationStore = DotNetOpenId.IAssociationStore<DotNetOpenId.AssociationRelyingPartyType>;
namespace DotNetOpenId.Provider {
/// <summary>
@@ -27,7 +28,7 @@ namespace DotNetOpenId.Provider {
/// before responding to the relying party's authentication request.
/// </remarks>
public static IAuthenticationRequest PendingAuthenticationRequest {
- get { return HttpContext.Current.Session[pendingAuthenticationRequestKey] as CheckIdRequest; }
+ get { return HttpContext.Current.Session[pendingAuthenticationRequestKey] as IAuthenticationRequest; }
set { HttpContext.Current.Session[pendingAuthenticationRequestKey] = value; }
}
@@ -48,6 +49,15 @@ namespace DotNetOpenId.Provider {
}
/// <summary>
+ /// A custom application store to use. 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 IProviderAssociationStore CustomApplicationStore { get; set; }
+
+ /// <summary>
/// Checks for incoming OpenID requests, responds to ones it can
/// respond to without policy checks, and fires events for custom
/// handling of the ones it cannot decide on automatically.
@@ -56,7 +66,14 @@ namespace DotNetOpenId.Provider {
base.OnLoad(e);
if (Enabled) {
- OpenIdProvider provider = new OpenIdProvider();
+ // Use the explicitly given state store 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 OpenIdProvider.
+ OpenIdProvider provider = new OpenIdProvider(
+ CustomApplicationStore ?? OpenIdProvider.Configuration.Store.CreateInstanceOfStore(OpenIdProvider.HttpApplicationStore),
+ OpenIdProvider.DefaultProviderEndpoint,
+ OpenIdProvider.DefaultRequestUrl,
+ OpenIdProvider.DefaultQuery);
// determine what incoming message was received
if (provider.Request != null) {
diff --git a/src/DotNetOpenId/Provider/ProviderSecuritySettings.cs b/src/DotNetOpenId/Provider/ProviderSecuritySettings.cs
new file mode 100644
index 0000000..88c1e07
--- /dev/null
+++ b/src/DotNetOpenId/Provider/ProviderSecuritySettings.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId.Provider {
+ sealed class ProviderSecuritySettings : SecuritySettings {
+ internal ProviderSecuritySettings() : base(true) { }
+
+ // This property is a placeholder for a feature that has not been written yet.
+ /// <summary>
+ /// Gets/sets whether OpenID 1.x relying parties that may not be
+ /// protecting their users from replay attacks are protected from
+ /// replay attacks by this provider.
+ /// </summary>
+ /// <remarks>
+ /// <para>Nonces for protection against replay attacks were not mandated
+ /// by OpenID 1.x, which leaves users open to replay attacks.</para>
+ /// <para>This feature works by preventing associations from being formed
+ /// with OpenID 1.x relying parties, thereby forcing them into
+ /// "dumb" mode and verifying every claim with this provider.
+ /// This gives the provider an opportunity to verify its own nonce
+ /// to protect against replay attacks.</para>
+ /// </remarks>
+ internal bool ProtectDownlevelReplayAttacks { get; set; }
+ }
+}
diff --git a/src/DotNetOpenId/Provider/ProviderSession.cs b/src/DotNetOpenId/Provider/ProviderSession.cs
index b524399..bbfd278 100644
--- a/src/DotNetOpenId/Provider/ProviderSession.cs
+++ b/src/DotNetOpenId/Provider/ProviderSession.cs
@@ -77,8 +77,8 @@ namespace DotNetOpenId.Provider {
sessionType = Util.GetRequiredArg(provider.Query, Protocol.openid.session_type);
Debug.Assert(Array.IndexOf(Protocol.Args.SessionType.AllDiffieHellman, sessionType) >= 0, "We should not have been invoked if this wasn't a recognized DH session request.");
- byte[] dh_modulus = Util.GetOptionalBase64Arg(Provider.Query, Protocol.openid.dh_modulus) ?? CryptUtil.DEFAULT_MOD;
- byte[] dh_gen = Util.GetOptionalBase64Arg(Provider.Query, Protocol.openid.dh_gen) ?? CryptUtil.DEFAULT_GEN;
+ byte[] dh_modulus = Util.GetOptionalBase64Arg(Provider.Query, Protocol.openid.dh_modulus) ?? DiffieHellmanUtil.DEFAULT_MOD;
+ byte[] dh_gen = Util.GetOptionalBase64Arg(Provider.Query, Protocol.openid.dh_gen) ?? DiffieHellmanUtil.DEFAULT_GEN;
dh = new DiffieHellmanManaged(dh_modulus, dh_gen, 1024);
consumerPublicKey = Util.GetRequiredBase64Arg(Provider.Query, Protocol.openid.dh_consumer_public);
@@ -89,13 +89,11 @@ namespace DotNetOpenId.Provider {
}
public override Dictionary<string, string> Answer(byte[] secret) {
- bool useSha256 = SessionType.Equals(Protocol.Args.SessionType.DH_SHA256, StringComparison.Ordinal);
- byte[] mac_key = CryptUtil.SHAHashXorSecret(
- useSha256 ? (HashAlgorithm) CryptUtil.Sha256 : CryptUtil.Sha1,
+ byte[] mac_key = DiffieHellmanUtil.SHAHashXorSecret(DiffieHellmanUtil.Lookup(Protocol, SessionType),
dh, consumerPublicKey, secret);
var nvc = new Dictionary<string, string>();
- nvc.Add(Protocol.openidnp.dh_server_public, CryptUtil.UnsignedToBase64(dh.CreateKeyExchange()));
+ nvc.Add(Protocol.openidnp.dh_server_public, DiffieHellmanUtil.UnsignedToBase64(dh.CreateKeyExchange()));
nvc.Add(Protocol.openidnp.enc_mac_key, Convert.ToBase64String(mac_key));
return nvc;
diff --git a/src/DotNetOpenId/Provider/Request.cs b/src/DotNetOpenId/Provider/Request.cs
index 9257e99..eb3edaf 100644
--- a/src/DotNetOpenId/Provider/Request.cs
+++ b/src/DotNetOpenId/Provider/Request.cs
@@ -84,10 +84,19 @@ namespace DotNetOpenId.Provider
}
/// <summary>
+ /// Gets the version of OpenID being used by the relying party that sent the request.
+ /// </summary>
+ public ProtocolVersion RelyingPartyVersion {
+ get {
+ return Protocol.Lookup(Protocol.Version).ProtocolVersion;
+ }
+ }
+
+ /// <summary>
/// Indicates whether this request has all the information necessary to formulate a response.
/// </summary>
public abstract bool IsResponseReady { get; }
- internal abstract IEncodable CreateResponse();
+ protected abstract IEncodable CreateResponse();
/// <summary>
/// Called whenever a property changes that would cause the response to need to be
/// regenerated if it had already been generated.
@@ -123,9 +132,36 @@ namespace DotNetOpenId.Provider
OutgoingExtensions.AddExtensionArguments(extension.TypeUri, extension.Serialize(this));
}
+ /// <summary>
+ /// Attempts to load an extension from an OpenId message.
+ /// </summary>
+ /// <param name="extension">The extension to attempt to load.</param>
+ /// <returns>
+ /// True if the extension was found in the message and successfully loaded.
+ /// False otherwise.
+ /// </returns>
+ bool getExtension(DotNetOpenId.Extensions.IExtensionRequest 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;
+ }
public T GetExtension<T>() where T : DotNetOpenId.Extensions.IExtensionRequest, new() {
T extension = new T();
- return extension.Deserialize(IncomingExtensions.GetExtensionArguments(extension.TypeUri), this) ? extension : default(T);
+ return getExtension(extension) ? (T)extension : default(T);
}
public DotNetOpenId.Extensions.IExtensionRequest GetExtension(Type extensionType) {
@@ -135,7 +171,7 @@ namespace DotNetOpenId.Provider
Strings.TypeMustImplementX, typeof(DotNetOpenId.Extensions.IExtensionRequest).FullName),
"extensionType");
var extension = (DotNetOpenId.Extensions.IExtensionRequest)Activator.CreateInstance(extensionType);
- return extension.Deserialize(IncomingExtensions.GetExtensionArguments(extension.TypeUri), this) ? extension : null;
+ return getExtension(extension) ? extension : null;
}
public override string ToString() {
diff --git a/src/DotNetOpenId/Provider/Signatory.cs b/src/DotNetOpenId/Provider/Signatory.cs
index 32b1945..eaf3d21 100644
--- a/src/DotNetOpenId/Provider/Signatory.cs
+++ b/src/DotNetOpenId/Provider/Signatory.cs
@@ -41,18 +41,14 @@ namespace DotNetOpenId.Provider {
assoc = GetAssociation(assoc_handle, AssociationRelyingPartyType.Smart);
if (assoc == null) {
- if (TraceUtil.Switch.TraceWarning) {
- Trace.TraceWarning("No associaton found with assoc_handle {0}. Setting invalidate_handle and creating new Association.", assoc_handle);
- }
+ Logger.WarnFormat("No associaton found with assoc_handle {0}. Setting invalidate_handle and creating new Association.", assoc_handle);
response.Fields[response.Protocol.openidnp.invalidate_handle] = assoc_handle;
assoc = CreateAssociation(AssociationRelyingPartyType.Dumb, null);
}
} else {
assoc = this.CreateAssociation(AssociationRelyingPartyType.Dumb, null);
- if (TraceUtil.Switch.TraceInfo) {
- Trace.TraceInformation("No assoc_handle supplied. Creating new association.");
- }
+ Logger.Debug("No assoc_handle supplied. Creating new association.");
}
response.Fields[response.Protocol.openidnp.assoc_handle] = assoc.Handle;
@@ -66,15 +62,14 @@ namespace DotNetOpenId.Provider {
public virtual bool Verify(string assoc_handle, string signature, IDictionary<string, string> signed_pairs, IList<string> signedKeyOrder) {
Association assoc = GetAssociation(assoc_handle, AssociationRelyingPartyType.Dumb);
if (assoc == null) {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Signature verification failed. No association with handle {0} found ", assoc_handle);
+ Logger.ErrorFormat("Signature verification failed. No association with handle {0} found ", assoc_handle);
return false;
}
string expected_sig = Convert.ToBase64String(assoc.Sign(signed_pairs, signedKeyOrder));
- if (TraceUtil.Switch.TraceError && signature != expected_sig) {
- Trace.TraceError("Expected signature is '{0}'. Actual signature is '{1}' ", expected_sig, signature);
+ if (signature != expected_sig) {
+ Logger.ErrorFormat("Expected signature is '{0}'. Actual signature is '{1}' ", expected_sig, signature);
}
return expected_sig.Equals(signature, StringComparison.Ordinal);
@@ -84,24 +79,25 @@ namespace DotNetOpenId.Provider {
if (provider == null && associationType == AssociationRelyingPartyType.Smart)
throw new ArgumentNullException("provider", "For Smart associations, the provider must be given.");
- bool useSha256;
string assoc_type;
+ Protocol associationProtocol;
if (associationType == AssociationRelyingPartyType.Dumb) {
- useSha256 = true;
- assoc_type = Protocol.v20.Args.SignatureAlgorithm.HMAC_SHA256;
+ // We'll just use the best association available.
+ associationProtocol = Protocol.Default;
+ assoc_type = associationProtocol.Args.SignatureAlgorithm.Best;
} else {
+ associationProtocol = provider.Protocol;
assoc_type = Util.GetRequiredArg(provider.Query, provider.Protocol.openid.assoc_type);
Debug.Assert(Array.IndexOf(provider.Protocol.Args.SignatureAlgorithm.All, assoc_type) >= 0, "This should have been checked by our caller.");
- useSha256 = assoc_type.Equals(provider.Protocol.Args.SignatureAlgorithm.HMAC_SHA256, StringComparison.Ordinal);
}
- int hashSize = useSha256 ? CryptUtil.Sha256.HashSize : CryptUtil.Sha1.HashSize;
+ int secretLength = HmacShaAssociation.GetSecretLength(associationProtocol, assoc_type);
RNGCryptoServiceProvider generator = new RNGCryptoServiceProvider();
- byte[] secret = new byte[hashSize / 8];
+ byte[] secret = new byte[secretLength];
byte[] uniq_bytes = new byte[4];
string uniq;
string handle;
- Association assoc;
+ HmacShaAssociation assoc;
generator.GetBytes(secret);
generator.GetBytes(uniq_bytes);
@@ -113,9 +109,7 @@ namespace DotNetOpenId.Provider {
handle = "{{" + assoc_type + "}{" + seconds + "}{" + uniq + "}";
TimeSpan lifeSpan = associationType == AssociationRelyingPartyType.Dumb ? dumbSecretLifetime : smartAssociationLifetime;
- assoc = useSha256 ? (Association)
- new HmacSha256Association(handle, secret, lifeSpan) :
- new HmacSha1Association(handle, secret, lifeSpan);
+ assoc = HmacShaAssociation.Create(secretLength, handle, secret, lifeSpan);
store.StoreAssociation(associationType, assoc);
@@ -128,8 +122,7 @@ namespace DotNetOpenId.Provider {
Association assoc = store.GetAssociation(associationType, assoc_handle);
if (assoc == null || assoc.IsExpired) {
- if (TraceUtil.Switch.TraceError)
- Trace.TraceError("Association {0} expired or not in store.", assoc_handle);
+ Logger.ErrorFormat("Association {0} expired or not in store.", assoc_handle);
store.RemoveAssociation(associationType, assoc_handle);
assoc = null;
}
@@ -138,8 +131,7 @@ namespace DotNetOpenId.Provider {
}
public virtual void Invalidate(string assoc_handle, AssociationRelyingPartyType associationType) {
- if (TraceUtil.Switch.TraceInfo)
- Trace.TraceInformation("Invalidating association '{0}'.", assoc_handle);
+ Logger.DebugFormat("Invalidating association '{0}'.", assoc_handle);
store.RemoveAssociation(associationType, assoc_handle);
}
diff --git a/src/DotNetOpenId/Provider/SigningMessageEncoder.cs b/src/DotNetOpenId/Provider/SigningMessageEncoder.cs
index ea5a522..f05f731 100644
--- a/src/DotNetOpenId/Provider/SigningMessageEncoder.cs
+++ b/src/DotNetOpenId/Provider/SigningMessageEncoder.cs
@@ -16,26 +16,14 @@ namespace DotNetOpenId.Provider {
}
public override Response Encode(IEncodable encodable) {
- OnSigning(encodable);
var response = encodable as EncodableResponse;
if (response != null) {
if (response.NeedsSigning) {
- Debug.Assert(!response.Fields.ContainsKey(encodable.Protocol.openidnp.sig));
signatory.Sign(response);
}
}
return base.Encode(encodable);
}
-
- /// <summary>
- /// Used for testing. Allows interception and modification of messages
- /// that are about to be returned to the RP.
- /// </summary>
- public static event EventHandler<EncodeEventArgs> Signing;
- protected virtual void OnSigning(IEncodable encodable) {
- if (Signing != null)
- Signing(this, new EncodeEventArgs(encodable));
- }
}
}
diff --git a/src/DotNetOpenId/Realm.cs b/src/DotNetOpenId/Realm.cs
index 22277a1..f7bb361 100644
--- a/src/DotNetOpenId/Realm.cs
+++ b/src/DotNetOpenId/Realm.cs
@@ -5,6 +5,7 @@ using System.Globalization;
using DotNetOpenId.Yadis;
using DotNetOpenId.Provider;
using System.Collections.Generic;
+using System.Xml;
namespace DotNetOpenId {
/// <summary>
@@ -225,7 +226,8 @@ namespace DotNetOpenId {
}
}
- // If path matches or is specified to root ...
+ // If path matches or is specified to root ...
+ // (deliberately case sensitive to protect security on case sensitive systems)
if (PathAndQuery.Equals(url.PathAndQuery, StringComparison.Ordinal)
|| PathAndQuery.Equals("/", StringComparison.Ordinal))
return true;
@@ -266,7 +268,7 @@ namespace DotNetOpenId {
/// <returns>The details of the endpoints if found, otherwise null.</returns>
internal IEnumerable<DotNetOpenId.Provider.RelyingPartyReceivingEndpoint> Discover(bool allowRedirects) {
// Attempt YADIS discovery
- DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww);
+ DiscoveryResult yadisResult = Yadis.Yadis.Discover(UriWithWildcardChangedToWww, false);
if (yadisResult != null) {
if (!allowRedirects && yadisResult.NormalizedUri != yadisResult.RequestUri) {
// Redirect occurred when it was not allowed.
@@ -274,11 +276,15 @@ namespace DotNetOpenId {
Strings.RealmCausedRedirectUponDiscovery, yadisResult.RequestUri));
}
if (yadisResult.IsXrds) {
- XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
- return xrds.FindRelyingPartyReceivingEndpoints();
+ try {
+ XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
+ return xrds.FindRelyingPartyReceivingEndpoints();
+ } catch (XmlException ex) {
+ throw new OpenIdException(Strings.InvalidXRDSDocument, ex);
+ }
}
}
- return new List<DotNetOpenId.Provider.RelyingPartyReceivingEndpoint>(); // empty list
+ return new RelyingPartyReceivingEndpoint[0];
}
/// <summary>
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
diff --git a/src/DotNetOpenId/Response.cs b/src/DotNetOpenId/Response.cs
index d4469ec..112aa4b 100644
--- a/src/DotNetOpenId/Response.cs
+++ b/src/DotNetOpenId/Response.cs
@@ -42,8 +42,7 @@ namespace DotNetOpenId {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.StatusCode = (int)Code;
- foreach (string headerName in Headers)
- HttpContext.Current.Response.AddHeader(headerName, Headers[headerName]);
+ Util.ApplyHeadersToResponse(Headers, HttpContext.Current.Response);
if (Body != null && Body.Length > 0) {
HttpContext.Current.Response.OutputStream.Write(Body, 0, Body.Length);
HttpContext.Current.Response.OutputStream.Flush();
diff --git a/src/DotNetOpenId/SecuritySettings.cs b/src/DotNetOpenId/SecuritySettings.cs
new file mode 100644
index 0000000..fb9c079
--- /dev/null
+++ b/src/DotNetOpenId/SecuritySettings.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DotNetOpenId {
+ /// <summary>
+ /// Security settings that may be applicable to both relying parties and providers.
+ /// </summary>
+ public class SecuritySettings {
+ internal SecuritySettings(bool isProvider) {
+ if (isProvider) {
+ maximumHashBitLength = maximumHashBitLengthOPDefault;
+ }
+ }
+
+ internal const int minimumHashBitLengthDefault = 160;
+ int minimumHashBitLength = minimumHashBitLengthDefault;
+ /// <summary>
+ /// Gets/sets the minimum hash length (in bits) allowed to be used in an <see cref="Association"/>
+ /// with the remote party. The default is 160.
+ /// </summary>
+ /// <remarks>
+ /// SHA-1 (160 bits) has been broken. The minimum secure hash length is now 256 bits.
+ /// The default is still a 160 bit minimum to allow interop with common remote parties,
+ /// such as Yahoo! that only supports 160 bits.
+ /// For sites that require high security such as to store bank account information and
+ /// health records, 256 is the recommended value.
+ /// </remarks>
+ public int MinimumHashBitLength {
+ get { return minimumHashBitLength; }
+ set { minimumHashBitLength = value; }
+ }
+ internal const int maximumHashBitLengthRPDefault = 256;
+ internal const int maximumHashBitLengthOPDefault = 512;
+ int maximumHashBitLength = maximumHashBitLengthRPDefault;
+ /// <summary>
+ /// Gets/sets the maximum hash length (in bits) allowed to be used in an <see cref="Association"/>
+ /// with the remote party. The default is 256 for relying parties and 512 for providers.
+ /// </summary>
+ /// <remarks>
+ /// The longer the bit length, the more secure the identities of your visitors are.
+ /// Setting a value higher than 256 on a relying party site may reduce performance
+ /// as many association requests will be denied, causing secondary requests or even
+ /// authentication failures.
+ /// Setting a value higher than 256 on a provider increases security where possible
+ /// without these side-effects.
+ /// </remarks>
+ public int MaximumHashBitLength {
+ get { return maximumHashBitLength; }
+ set { maximumHashBitLength = value; }
+ }
+
+ internal bool IsAssociationInPermittedRange(Protocol protocol, string associationType) {
+ int lengthInBits = HmacShaAssociation.GetSecretLength(protocol, associationType) * 8;
+ return lengthInBits >= MinimumHashBitLength && lengthInBits <= MaximumHashBitLength;
+ }
+ }
+}
diff --git a/src/DotNetOpenId/Strings.Designer.cs b/src/DotNetOpenId/Strings.Designer.cs
index eac43d2..0af791f 100644
--- a/src/DotNetOpenId/Strings.Designer.cs
+++ b/src/DotNetOpenId/Strings.Designer.cs
@@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
-// Runtime Version:2.0.50727.1434
+// Runtime Version:2.0.50727.3521
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -88,6 +88,69 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to A Simple Registration request must be deserialized before CreateResponse can be called..
+ /// </summary>
+ internal static string CallDeserializeBeforeCreateResponse {
+ get {
+ return ResourceManager.GetString("CallDeserializeBeforeCreateResponse", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to XRI CanonicalID verification failed..
+ /// </summary>
+ internal static string CIDVerificationFailed {
+ get {
+ return ResourceManager.GetString("CIDVerificationFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ClaimedIdentifier property cannot be set when IsDelegatedIdentifier is true to avoid breaking OpenID URL delegation..
+ /// </summary>
+ internal static string ClaimedIdentifierCannotBeSetOnDelegatedAuthentication {
+ get {
+ return ResourceManager.GetString("ClaimedIdentifierCannotBeSetOnDelegatedAuthentication", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The ClaimedIdentifier property must be set first..
+ /// </summary>
+ internal static string ClaimedIdentifierMustBeSetFirst {
+ get {
+ return ResourceManager.GetString("ClaimedIdentifierMustBeSetFirst", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An extension with this property name (&apos;{0}&apos;) has already been registered..
+ /// </summary>
+ internal static string ClientScriptExtensionPropertyNameCollision {
+ get {
+ return ResourceManager.GetString("ClientScriptExtensionPropertyNameCollision", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The extension &apos;{0}&apos; has already been registered..
+ /// </summary>
+ internal static string ClientScriptExtensionTypeCollision {
+ get {
+ return ResourceManager.GetString("ClientScriptExtensionTypeCollision", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to An authentication request has already been created using CreateRequest()..
+ /// </summary>
+ internal static string CreateRequestAlreadyCalled {
+ get {
+ return ResourceManager.GetString("CreateRequestAlreadyCalled", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to HttpContext.Current is null. There must be an ASP.NET request in process for this operation to succeed..
/// </summary>
internal static string CurrentHttpContextRequired {
@@ -115,7 +178,7 @@ namespace DotNetOpenId {
}
/// <summary>
- /// Looks up a localized string similar to The nonce has expired..
+ /// Looks up a localized string similar to The nonce has expired. It was good until {0} (UTC), and it is now {1} (UTC). If this looks wrong, check the server&apos;s clock, time zone and daylight savings settings..
/// </summary>
internal static string ExpiredNonce {
get {
@@ -124,6 +187,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to URI is not SSL yet requireSslDiscovery is set to true..
+ /// </summary>
+ internal static string ExplicitHttpUriSuppliedWithSslRequirement {
+ get {
+ return ResourceManager.GetString("ExplicitHttpUriSuppliedWithSslRequirement", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to An extension sharing namespace &apos;{0}&apos; has already been added. Only one extension per namespace is allowed in a given request..
/// </summary>
internal static string ExtensionAlreadyAddedWithSameTypeURI {
@@ -151,6 +223,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to Fragment segments do not apply to XRI identifiers..
+ /// </summary>
+ internal static string FragmentNotAllowedOnXRIs {
+ get {
+ return ResourceManager.GetString("FragmentNotAllowedOnXRIs", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to No current ASP.NET HttpContext was detected. Use an overload that does not require one..
/// </summary>
internal static string HttpContextRequiredForThisOverload {
@@ -169,25 +250,43 @@ namespace DotNetOpenId {
}
/// <summary>
- /// Looks up a localized string similar to This operation is only allowed when IsIdentifierSelect is true..
+ /// Looks up a localized string similar to ClaimedIdentifier and LocalIdentifier must be the same when IsIdentifierSelect is true..
/// </summary>
- internal static string IdentifierSelectModeOnly {
+ internal static string IdentifierSelectRequiresMatchingIdentifiers {
get {
- return ResourceManager.GetString("IdentifierSelectModeOnly", resourceCulture);
+ return ResourceManager.GetString("IdentifierSelectRequiresMatchingIdentifiers", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to ClaimedIdentifier and LocalIdentifier must be the same when IsIdentifierSelect is true..
+ /// Looks up a localized string similar to The Provider requested association type &apos;{0}&apos; and session type &apos;{1}&apos;, which are not compatible with each other..
/// </summary>
- internal static string IdentifierSelectRequiresMatchingIdentifiers {
+ internal static string IncompatibleAssociationAndSessionTypes {
get {
- return ResourceManager.GetString("IdentifierSelectRequiresMatchingIdentifiers", resourceCulture);
+ return ResourceManager.GetString("IncompatibleAssociationAndSessionTypes", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Inconsistent setting of application state. Authentication request was sent with application state available, but authentication response was received without it available. This makes it impossible to validate the token&apos;s signature and will cause assertion verification failure..
+ /// </summary>
+ internal static string InconsistentAppState {
+ get {
+ return ResourceManager.GetString("InconsistentAppState", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Insecure web request for &apos;{0}&apos; aborted due to security requirements demanding HTTPS..
+ /// </summary>
+ internal static string InsecureWebRequestWithSslRequired {
+ get {
+ return ResourceManager.GetString("InsecureWebRequestWithSslRequired", resourceCulture);
}
}
/// <summary>
- /// Looks up a localized string similar to Cannot encode &apos;{0}&apos; because it contains an illegal character for Key-Value Form encoding..
+ /// Looks up a localized string similar to Cannot encode &apos;{0}&apos; because it contains an illegal character for Key-Value Form encoding. (line {1}: &apos;{2}&apos;).
/// </summary>
internal static string InvalidCharacterInKeyValueFormInput {
get {
@@ -196,7 +295,7 @@ namespace DotNetOpenId {
}
/// <summary>
- /// Looks up a localized string similar to Cannot decode Key-Value Form because a line was found without a &apos;{0}&apos; character..
+ /// Looks up a localized string similar to Cannot decode Key-Value Form because a line was found without a &apos;{0}&apos; character. (line {1}: &apos;{2}&apos;).
/// </summary>
internal static string InvalidKeyValueFormCharacterMissing {
get {
@@ -268,6 +367,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to Failure parsing XRDS document..
+ /// </summary>
+ internal static string InvalidXRDSDocument {
+ get {
+ return ResourceManager.GetString("InvalidXRDSDocument", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Not a recognized XRI format: &apos;{0}&apos;..
/// </summary>
internal static string InvalidXri {
@@ -277,7 +385,11 @@ namespace DotNetOpenId {
}
/// <summary>
- /// Looks up a localized string similar to The OpenId Provider issued an assertion for an Identifier whose discovery information did not match..
+ /// Looks up a localized string similar to The OpenId Provider issued an assertion for an Identifier whose discovery information did not match.
+ ///Assertion endpoint info:
+ ///{0}
+ ///Discovered endpoint info:
+ ///{1}.
/// </summary>
internal static string IssuedAssertionFailsIdentifierDiscovery {
get {
@@ -313,7 +425,16 @@ namespace DotNetOpenId {
}
/// <summary>
- /// Looks up a localized string similar to The XRDS document is missing the required CanonicalID element..
+ /// Looks up a localized string similar to This Relying Party requires a Provider that supports at least OpenID version {0}, but Provider is detected to only support OpenID version {1}..
+ /// </summary>
+ internal static string MinimumOPVersionRequirementNotMet {
+ get {
+ return ResourceManager.GetString("MinimumOPVersionRequirementNotMet", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to The XRDS document for XRI {0} is missing the required CanonicalID element..
/// </summary>
internal static string MissingCanonicalIDElement {
get {
@@ -358,6 +479,24 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to This operation is not supported by serialized authentication responses. Try this operation from the LoggedIn event handler..
+ /// </summary>
+ internal static string NotSupportedByAuthenticationSnapshot {
+ get {
+ return ResourceManager.GetString("NotSupportedByAuthenticationSnapshot", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Positive assertion sent with OpenID version {0} but Identifier discovery suggested it would be {1}..
+ /// </summary>
+ internal static string OpenIdDiscoveredAndActualVersionMismatch {
+ get {
+ return ResourceManager.GetString("OpenIdDiscoveredAndActualVersionMismatch", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to No OpenId endpoint found..
/// </summary>
internal static string OpenIdEndpointNotFound {
@@ -376,6 +515,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to This operation is only allowed when IAuthenticationResponse.State == AuthenticationStatus.SetupRequired..
+ /// </summary>
+ internal static string OperationOnlyValidForSetupRequiredState {
+ get {
+ return ResourceManager.GetString("OperationOnlyValidForSetupRequiredState", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Prefix should not begin or end with a period..
/// </summary>
internal static string PrefixWithoutPeriodsExpected {
@@ -457,7 +605,7 @@ namespace DotNetOpenId {
}
/// <summary>
- /// Looks up a localized string similar to The {0} parameter does not match the actual URL the request was made with..
+ /// Looks up a localized string similar to The {0} parameter ({1}) does not match the actual URL ({2}) the request was made with..
/// </summary>
internal static string ReturnToParamDoesNotMatchRequestUrl {
get {
@@ -475,6 +623,15 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Looks up a localized string similar to The maximum allowable number of redirects were exceeded while requesting &apos;{0}&apos;..
+ /// </summary>
+ internal static string TooManyRedirects {
+ get {
+ return ResourceManager.GetString("TooManyRedirects", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to The type must implement {0}..
/// </summary>
internal static string TypeMustImplementX {
@@ -500,5 +657,32 @@ namespace DotNetOpenId {
return ResourceManager.GetString("UnspecifiedDateTimeKindNotAllowed", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to Web request to &apos;{0}&apos; failed..
+ /// </summary>
+ internal static string WebRequestFailed {
+ get {
+ return ResourceManager.GetString("WebRequestFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to XRI resolution failed..
+ /// </summary>
+ internal static string XriResolutionFailed {
+ get {
+ return ResourceManager.GetString("XriResolutionFailed", resourceCulture);
+ }
+ }
+
+ /// <summary>
+ /// Looks up a localized string similar to Could not find XRI resolution Status tag or code attribute was invalid..
+ /// </summary>
+ internal static string XriResolutionStatusMissing {
+ get {
+ return ResourceManager.GetString("XriResolutionStatusMissing", resourceCulture);
+ }
+ }
}
}
diff --git a/src/DotNetOpenId/Strings.resx b/src/DotNetOpenId/Strings.resx
index f2250ce..6bebc76 100644
--- a/src/DotNetOpenId/Strings.resx
+++ b/src/DotNetOpenId/Strings.resx
@@ -126,6 +126,24 @@
<data name="BadAssociationPrivateData" xml:space="preserve">
<value>The private data supplied does not meet the requirements of any known Association type. Its length may be too short, or it may have been corrupted.</value>
</data>
+ <data name="CallDeserializeBeforeCreateResponse" xml:space="preserve">
+ <value>A Simple Registration request must be deserialized before CreateResponse can be called.</value>
+ </data>
+ <data name="CIDVerificationFailed" xml:space="preserve">
+ <value>XRI CanonicalID verification failed.</value>
+ </data>
+ <data name="ClaimedIdentifierMustBeSetFirst" xml:space="preserve">
+ <value>The ClaimedIdentifier property must be set first.</value>
+ </data>
+ <data name="ClientScriptExtensionPropertyNameCollision" xml:space="preserve">
+ <value>An extension with this property name ('{0}') has already been registered.</value>
+ </data>
+ <data name="ClientScriptExtensionTypeCollision" xml:space="preserve">
+ <value>The extension '{0}' has already been registered.</value>
+ </data>
+ <data name="CreateRequestAlreadyCalled" xml:space="preserve">
+ <value>An authentication request has already been created using CreateRequest().</value>
+ </data>
<data name="CurrentHttpContextRequired" xml:space="preserve">
<value>HttpContext.Current is null. There must be an ASP.NET request in process for this operation to succeed.</value>
</data>
@@ -136,7 +154,10 @@
<value>OpenID parameter '{0}' was expected to be base64 encoded but is not.</value>
</data>
<data name="ExpiredNonce" xml:space="preserve">
- <value>The nonce has expired.</value>
+ <value>The nonce has expired. It was good until {0} (UTC), and it is now {1} (UTC). If this looks wrong, check the server's clock, time zone and daylight savings settings.</value>
+ </data>
+ <data name="ExplicitHttpUriSuppliedWithSslRequirement" xml:space="preserve">
+ <value>URI is not SSL yet requireSslDiscovery is set to true.</value>
</data>
<data name="ExtensionAlreadyAddedWithSameTypeURI" xml:space="preserve">
<value>An extension sharing namespace '{0}' has already been added. Only one extension per namespace is allowed in a given request.</value>
@@ -147,23 +168,32 @@
<data name="FieldMustBeSigned" xml:space="preserve">
<value>The OpenID parameter '{0}' must be signed by the OpenID Provider, but was not.</value>
</data>
+ <data name="FragmentNotAllowedOnXRIs" xml:space="preserve">
+ <value>Fragment segments do not apply to XRI identifiers.</value>
+ </data>
<data name="HttpContextRequiredForThisOverload" xml:space="preserve">
<value>No current ASP.NET HttpContext was detected. Use an overload that does not require one.</value>
</data>
<data name="IAssociationStoreRequiredWhenNoHttpContextAvailable" xml:space="preserve">
<value>No current HttpContext was detected, so an IAssociationStore must be explicitly provided. Call the Server constructor overload that takes an IAssociationStore.</value>
</data>
- <data name="IdentifierSelectModeOnly" xml:space="preserve">
- <value>This operation is only allowed when IsIdentifierSelect is true.</value>
- </data>
<data name="IdentifierSelectRequiresMatchingIdentifiers" xml:space="preserve">
<value>ClaimedIdentifier and LocalIdentifier must be the same when IsIdentifierSelect is true.</value>
</data>
+ <data name="IncompatibleAssociationAndSessionTypes" xml:space="preserve">
+ <value>The Provider requested association type '{0}' and session type '{1}', which are not compatible with each other.</value>
+ </data>
+ <data name="InconsistentAppState" xml:space="preserve">
+ <value>Inconsistent setting of application state. Authentication request was sent with application state available, but authentication response was received without it available. This makes it impossible to validate the token's signature and will cause assertion verification failure.</value>
+ </data>
+ <data name="InsecureWebRequestWithSslRequired" xml:space="preserve">
+ <value>Insecure web request for '{0}' aborted due to security requirements demanding HTTPS.</value>
+ </data>
<data name="InvalidCharacterInKeyValueFormInput" xml:space="preserve">
- <value>Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding.</value>
+ <value>Cannot encode '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}')</value>
</data>
<data name="InvalidKeyValueFormCharacterMissing" xml:space="preserve">
- <value>Cannot decode Key-Value Form because a line was found without a '{0}' character.</value>
+ <value>Cannot decode Key-Value Form because a line was found without a '{0}' character. (line {1}: '{2}')</value>
</data>
<data name="InvalidNonce" xml:space="preserve">
<value>The nonce was not in the expected format.</value>
@@ -186,11 +216,18 @@
<data name="InvalidUri" xml:space="preserve">
<value>The value '{0}' is not a valid URI.</value>
</data>
+ <data name="InvalidXRDSDocument" xml:space="preserve">
+ <value>Failure parsing XRDS document.</value>
+ </data>
<data name="InvalidXri" xml:space="preserve">
<value>Not a recognized XRI format: '{0}'.</value>
</data>
<data name="IssuedAssertionFailsIdentifierDiscovery" xml:space="preserve">
- <value>The OpenId Provider issued an assertion for an Identifier whose discovery information did not match.</value>
+ <value>The OpenId Provider issued an assertion for an Identifier whose discovery information did not match.
+Assertion endpoint info:
+{0}
+Discovered endpoint info:
+{1}</value>
</data>
<data name="KeyAlreadyExists" xml:space="preserve">
<value>The given key '{0}' already exists.</value>
@@ -201,8 +238,11 @@
<data name="MatchingArgumentsExpected" xml:space="preserve">
<value>The '{0}' and '{1}' parameters must both be or not be '{2}'.</value>
</data>
+ <data name="MinimumOPVersionRequirementNotMet" xml:space="preserve">
+ <value>This Relying Party requires a Provider that supports at least OpenID version {0}, but Provider is detected to only support OpenID version {1}.</value>
+ </data>
<data name="MissingCanonicalIDElement" xml:space="preserve">
- <value>The XRDS document is missing the required CanonicalID element.</value>
+ <value>The XRDS document for XRI {0} is missing the required CanonicalID element.</value>
</data>
<data name="MissingInternalQueryParameter" xml:space="preserve">
<value>Query parameter '{0}' was missing from the query.</value>
@@ -216,12 +256,18 @@
<data name="NoRelyingPartyEndpointDiscovered" xml:space="preserve">
<value>No XRDS document containing OpenId relying party endpoint information could be found at {0}.</value>
</data>
+ <data name="NotSupportedByAuthenticationSnapshot" xml:space="preserve">
+ <value>This operation is not supported by serialized authentication responses. Try this operation from the LoggedIn event handler.</value>
+ </data>
<data name="OpenIdEndpointNotFound" xml:space="preserve">
<value>No OpenId endpoint found.</value>
</data>
<data name="OpenIdTextBoxEmpty" xml:space="preserve">
<value>No OpenId url is provided.</value>
</data>
+ <data name="OperationOnlyValidForSetupRequiredState" xml:space="preserve">
+ <value>This operation is only allowed when IAuthenticationResponse.State == AuthenticationStatus.SetupRequired.</value>
+ </data>
<data name="PrefixWithoutPeriodsExpected" xml:space="preserve">
<value>Prefix should not begin or end with a period.</value>
</data>
@@ -250,11 +296,14 @@
<value>return_to '{0}' not under realm '{1}'.</value>
</data>
<data name="ReturnToParamDoesNotMatchRequestUrl" xml:space="preserve">
- <value>The {0} parameter does not match the actual URL the request was made with.</value>
+ <value>The {0} parameter ({1}) does not match the actual URL ({2}) the request was made with.</value>
</data>
<data name="TamperingDetected" xml:space="preserve">
<value>The '{0}' parameter was expected to have the value '{1}' but had '{2}' instead.</value>
</data>
+ <data name="TooManyRedirects" xml:space="preserve">
+ <value>The maximum allowable number of redirects were exceeded while requesting '{0}'.</value>
+ </data>
<data name="TypeMustImplementX" xml:space="preserve">
<value>The type must implement {0}.</value>
</data>
@@ -264,4 +313,19 @@
<data name="UnspecifiedDateTimeKindNotAllowed" xml:space="preserve">
<value>Providing a DateTime whose Kind is Unspecified is not allowed.</value>
</data>
+ <data name="WebRequestFailed" xml:space="preserve">
+ <value>Web request to '{0}' failed.</value>
+ </data>
+ <data name="XriResolutionFailed" xml:space="preserve">
+ <value>XRI resolution failed.</value>
+ </data>
+ <data name="XriResolutionStatusMissing" xml:space="preserve">
+ <value>Could not find XRI resolution Status tag or code attribute was invalid.</value>
+ </data>
+ <data name="ClaimedIdentifierCannotBeSetOnDelegatedAuthentication" xml:space="preserve">
+ <value>The ClaimedIdentifier property cannot be set when IsDelegatedIdentifier is true to avoid breaking OpenID URL delegation.</value>
+ </data>
+ <data name="OpenIdDiscoveredAndActualVersionMismatch" xml:space="preserve">
+ <value>Positive assertion sent with OpenID version {0} but Identifier discovery suggested it would be {1}.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/DotNetOpenId/TraceUtil.cs b/src/DotNetOpenId/TraceUtil.cs
deleted file mode 100644
index 5c6a57e..0000000
--- a/src/DotNetOpenId/TraceUtil.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Diagnostics;
-using System.IO;
-using System.Text;
-using System.Xml.Serialization;
-using System.Security;
-using System.Globalization;
-
-namespace DotNetOpenId {
- internal class TraceUtil {
- static TraceSwitch openIDTraceSwitch;
- /// <summary>
- /// Gets the switch that indicates whether the user of this library desires for trace messages
- /// to be emitted.
- /// </summary>
- public static TraceSwitch Switch {
- get {
- if (openIDTraceSwitch == null) { openIDTraceSwitch = new TraceSwitch("OpenID", "OpenID Trace Switch"); }
- return openIDTraceSwitch;
- }
- }
-
- /// <summary>
- /// Serialize obj to an xml string.
- /// </summary>
- public static string ToString(object obj) {
- XmlSerializer serializer = new XmlSerializer(obj.GetType());
- using (StringWriter writer = new StringWriter(CultureInfo.CurrentCulture)) {
- serializer.Serialize(writer, obj);
- return writer.ToString();
- }
- }
-
- public static string ToString(NameValueCollection collection) {
- using (StringWriter sw = new StringWriter(CultureInfo.CurrentCulture)) {
- foreach (string key in collection.Keys) {
- sw.WriteLine("{0} = '{1}'", key, collection[key]);
- }
- return sw.ToString();
- }
- }
- }
-}
diff --git a/src/DotNetOpenId/UntrustedWebRequest.cs b/src/DotNetOpenId/UntrustedWebRequest.cs
index f169a89..a621a65 100644
--- a/src/DotNetOpenId/UntrustedWebRequest.cs
+++ b/src/DotNetOpenId/UntrustedWebRequest.cs
@@ -3,13 +3,15 @@
#endif
namespace DotNetOpenId {
using System;
- using System.Net;
- using System.IO;
+ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
- using System.Collections.Generic;
+ using System.IO;
+ using System.Net;
using System.Text.RegularExpressions;
-
+ using System.Configuration;
+ using DotNetOpenId.Configuration;
+ using System.Reflection;
/// <summary>
/// A paranoid HTTP get/post request engine. It helps to protect against attacks from remote
/// server leaving dangling connections, sending too much data, causing requests against
@@ -25,8 +27,14 @@ namespace DotNetOpenId {
/// If a particular host would not be permitted but is in the whitelist, it is allowed.
/// </remarks>
public static class UntrustedWebRequest {
+ private static string UserAgentValue = Assembly.GetExecutingAssembly().GetName().Name + "/" + Assembly.GetExecutingAssembly().GetName().Version;
+
+ static Configuration.UntrustedWebRequestSection Configuration {
+ get { return UntrustedWebRequestSection.Configuration; }
+ }
+
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
- static int maximumBytesToRead = 1024 * 1024;
+ static int maximumBytesToRead = Configuration.MaximumBytesToRead;
/// <summary>
/// The default maximum bytes to read in any given HTTP request.
/// Default is 1MB. Cannot be less than 2KB.
@@ -39,7 +47,7 @@ namespace DotNetOpenId {
}
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
- static int maximumRedirections = 10;
+ static int maximumRedirections = Configuration.MaximumRedirections;
/// <summary>
/// The total number of redirections to allow on any one request.
/// Default is 10.
@@ -62,10 +70,20 @@ namespace DotNetOpenId {
/// </summary>
public static TimeSpan Timeout { get; set; }
+ internal delegate UntrustedWebResponse MockRequestResponse(Uri uri, byte[] body, string[] acceptTypes);
+ /// <summary>
+ /// Used in unit testing to mock HTTP responses to expected requests.
+ /// </summary>
+ /// <remarks>
+ /// If null, no mocking will take place. But if non-null, all requests
+ /// will be channeled through this mock method for processing.
+ /// </remarks>
+ internal static MockRequestResponse MockRequests;
+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static UntrustedWebRequest() {
- ReadWriteTimeout = TimeSpan.FromMilliseconds(500);
- Timeout = TimeSpan.FromSeconds(10);
+ ReadWriteTimeout = Configuration.ReadWriteTimeout;
+ Timeout = Configuration.Timeout;
#if LONGTIMEOUT
ReadWriteTimeout = TimeSpan.FromHours(1);
Timeout = TimeSpan.FromHours(1);
@@ -81,25 +99,25 @@ namespace DotNetOpenId {
return true;
}
static ICollection<string> allowableSchemes = new List<string> { "http", "https" };
- static ICollection<string> whitelistHosts = new List<string>();
+ static ICollection<string> whitelistHosts = new List<string>(Configuration.WhitelistHosts.KeysAsStrings);
/// <summary>
/// A collection of host name literals that should be allowed even if they don't
/// pass standard security checks.
/// </summary>
public static ICollection<string> WhitelistHosts { get { return whitelistHosts; } }
- static ICollection<Regex> whitelistHostsRegex = new List<Regex>();
+ static ICollection<Regex> whitelistHostsRegex = new List<Regex>(Configuration.WhitelistHostsRegex.KeysAsRegexs);
/// <summary>
/// A collection of host name regular expressions that indicate hosts that should
/// be allowed even though they don't pass standard security checks.
/// </summary>
public static ICollection<Regex> WhitelistHostsRegex { get { return whitelistHostsRegex; } }
- static ICollection<string> blacklistHosts = new List<string>();
+ static ICollection<string> blacklistHosts = new List<string>(Configuration.BlacklistHosts.KeysAsStrings);
/// <summary>
/// A collection of host name literals that should be rejected even if they
/// pass standard security checks.
/// </summary>
public static ICollection<string> BlacklistHosts { get { return blacklistHosts; } }
- static ICollection<Regex> blacklistHostsRegex = new List<Regex>();
+ static ICollection<Regex> blacklistHostsRegex = new List<Regex>(Configuration.BlacklistHostsRegex.KeysAsRegexs);
/// <summary>
/// A collection of host name regular expressions that indicate hosts that should
/// be rjected even if they pass standard security checks.
@@ -128,16 +146,14 @@ namespace DotNetOpenId {
static bool isUriAllowable(Uri uri) {
Debug.Assert(uri != null);
if (!allowableSchemes.Contains(uri.Scheme)) {
- if (TraceUtil.Switch.TraceWarning)
- Trace.TraceWarning("Rejecting URL {0} because it uses a disallowed scheme.", uri);
+ Logger.WarnFormat("Rejecting URL {0} because it uses a disallowed scheme.", uri);
return false;
}
// Allow for whitelist or blacklist to override our detection.
DotNetOpenId.Util.Func<string, bool> failsUnlessWhitelisted = (string reason) => {
if (isHostWhitelisted(uri.DnsSafeHost)) return true;
- if (TraceUtil.Switch.TraceWarning)
- Trace.TraceWarning("Rejecting URL {0} because {1}.", uri, reason);
+ Logger.WarnFormat("Rejecting URL {0} because {1}.", uri, reason);
return false;
};
@@ -171,8 +187,7 @@ namespace DotNetOpenId {
}
}
if (isHostBlacklisted(uri.DnsSafeHost)) {
- if (TraceUtil.Switch.TraceWarning)
- Trace.TraceWarning("Rejected URL {0} because it is blacklisted.", uri);
+ Logger.WarnFormat("Rejected URL {0} because it is blacklisted.", uri);
return false;
}
return true;
@@ -198,11 +213,11 @@ namespace DotNetOpenId {
}
}
- static UntrustedWebResponse getResponse(Uri requestUri, HttpWebResponse resp) {
+ static UntrustedWebResponse getResponse(Uri requestUri, Uri finalRequestUri, HttpWebResponse resp) {
byte[] data;
int length;
readData(resp, out data, out length);
- return new UntrustedWebResponse(requestUri, resp, new MemoryStream(data, 0, length));
+ return new UntrustedWebResponse(requestUri, finalRequestUri, resp, new MemoryStream(data, 0, length));
}
internal static UntrustedWebResponse Request(Uri uri) {
@@ -217,17 +232,53 @@ namespace DotNetOpenId {
return Request(uri, body, acceptTypes, false);
}
- static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes,
- bool avoidSendingExpect100Continue) {
+ internal static UntrustedWebResponse Request(Uri uri, byte[] body, string[] acceptTypes, bool requireSsl) {
+ // Since we may require SSL for every redirect, we handle each redirect manually
+ // in order to detect and fail if any redirect sends us to an HTTP url.
+ // We COULD allow automatic redirect in the cases where HTTPS is not required,
+ // but our mock request infrastructure can't do redirects on its own either.
+ Uri originalRequestUri = uri;
+ int i;
+ for (i = 0; i < MaximumRedirections; i++) {
+ UntrustedWebResponse response = RequestInternal(uri, body, acceptTypes, requireSsl, false, originalRequestUri);
+ if (response.StatusCode == HttpStatusCode.MovedPermanently ||
+ response.StatusCode == HttpStatusCode.Redirect ||
+ response.StatusCode == HttpStatusCode.RedirectMethod ||
+ response.StatusCode == HttpStatusCode.RedirectKeepVerb) {
+ uri = new Uri(response.FinalUri, response.Headers[HttpResponseHeader.Location]);
+ } else {
+ return response;
+ }
+ }
+ throw new WebException(string.Format(CultureInfo.CurrentCulture, Strings.TooManyRedirects, originalRequestUri));
+ }
+
+ static UntrustedWebResponse RequestInternal(Uri uri, byte[] body, string[] acceptTypes,
+ bool requireSsl, bool avoidSendingExpect100Continue, Uri originalRequestUri) {
if (uri == null) throw new ArgumentNullException("uri");
+ if (originalRequestUri == null) throw new ArgumentNullException("originalRequestUri");
if (!isUriAllowable(uri)) throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.UnsafeWebRequestDetected, uri), "uri");
+ if (requireSsl && !String.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture, Strings.InsecureWebRequestWithSslRequired, uri));
+ }
+
+ // mock the request if a hosting unit test has configured it.
+ if (MockRequests != null) {
+ return MockRequests(uri, body, acceptTypes);
+ }
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
+ // If SSL is required throughout, we cannot allow auto redirects because
+ // it may include a pass through an unprotected HTTP request.
+ // We have to follow redirects manually, and our caller will be responsible for that.
+ // It also allows us to ignore HttpWebResponse.FinalUri since that can be affected by
+ // the Content-Location header and open security holes.
+ request.AllowAutoRedirect = false;
request.ReadWriteTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
request.Timeout = (int)Timeout.TotalMilliseconds;
request.KeepAlive = false;
- request.MaximumAutomaticRedirections = MaximumRedirections;
+ request.UserAgent = UserAgentValue;
if (acceptTypes != null)
request.Accept = string.Join(",", acceptTypes);
if (body != null) {
@@ -255,19 +306,20 @@ namespace DotNetOpenId {
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
- return getResponse(uri, response);
+ return getResponse(originalRequestUri, request.RequestUri, response);
}
} catch (WebException e) {
using (HttpWebResponse response = (HttpWebResponse)e.Response) {
if (response != null) {
if (response.StatusCode == HttpStatusCode.ExpectationFailed) {
if (!avoidSendingExpect100Continue) { // must only try this once more
- return Request(uri, body, acceptTypes, true);
+ return RequestInternal(uri, body, acceptTypes, requireSsl, true, originalRequestUri);
}
}
- return getResponse(uri, response);
+ return getResponse(originalRequestUri, request.RequestUri, response);
} else {
- throw;
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.WebRequestFailed, originalRequestUri), e);
}
}
}
diff --git a/src/DotNetOpenId/UntrustedWebResponse.cs b/src/DotNetOpenId/UntrustedWebResponse.cs
index 1fcdedd..0834125 100644
--- a/src/DotNetOpenId/UntrustedWebResponse.cs
+++ b/src/DotNetOpenId/UntrustedWebResponse.cs
@@ -6,6 +6,7 @@ namespace DotNetOpenId {
using System.Text;
using System.Net.Mime;
using System.Diagnostics;
+ using System.Globalization;
[Serializable]
[DebuggerDisplay("{StatusCode} {ContentType.MediaType}: {ReadResponseString().Substring(4,50)}")]
@@ -20,18 +21,46 @@ namespace DotNetOpenId {
public Uri RequestUri { get; private set; }
public Uri FinalUri { get; private set; }
- public UntrustedWebResponse(Uri requestUri, HttpWebResponse response, Stream responseStream) {
+ public UntrustedWebResponse(Uri requestUri, Uri finalRequestUri, HttpWebResponse response, Stream responseStream) {
if (requestUri == null) throw new ArgumentNullException("requestUri");
+ if (finalRequestUri == null) throw new ArgumentNullException("finalRequestUri");
if (response == null) throw new ArgumentNullException("response");
if (responseStream == null) throw new ArgumentNullException("responseStream");
this.RequestUri = requestUri;
this.ResponseStream = responseStream;
StatusCode = response.StatusCode;
- if (!string.IsNullOrEmpty(response.ContentType))
- ContentType = new ContentType(response.ContentType);
+ if (!string.IsNullOrEmpty(response.ContentType)) {
+ try {
+ ContentType = new ContentType(response.ContentType);
+ } catch (FormatException) {
+ Logger.ErrorFormat("HTTP response to {0} included an invalid Content-Type header value: {1}", response.ResponseUri.AbsoluteUri, response.ContentType);
+ }
+ }
ContentEncoding = string.IsNullOrEmpty(response.ContentEncoding) ? DefaultContentEncoding : response.ContentEncoding;
Headers = response.Headers;
- FinalUri = response.ResponseUri;
+ FinalUri = finalRequestUri;
+ }
+
+ /// <summary>
+ /// Constructs a mock web response.
+ /// </summary>
+ internal UntrustedWebResponse(Uri requestUri, Uri responseUri, WebHeaderCollection headers,
+ HttpStatusCode statusCode, string contentType, string contentEncoding, Stream responseStream) {
+ if (requestUri == null) throw new ArgumentNullException("requestUri");
+ if (responseStream == null) throw new ArgumentNullException("responseStream");
+ RequestUri = requestUri;
+ ResponseStream = responseStream;
+ StatusCode = statusCode;
+ if (!string.IsNullOrEmpty(contentType)) {
+ try {
+ ContentType = new ContentType(contentType);
+ } catch (FormatException) {
+ Logger.ErrorFormat("HTTP response to {0} included an invalid Content-Type header value: {1}", responseUri.AbsoluteUri, contentType);
+ }
+ }
+ ContentEncoding = string.IsNullOrEmpty(contentEncoding) ? DefaultContentEncoding : contentEncoding;
+ Headers = headers;
+ FinalUri = responseUri;
}
public string ReadResponseString() {
@@ -44,5 +73,21 @@ namespace DotNetOpenId {
ResponseStream.Seek(oldPosition, SeekOrigin.Begin);
return result;
}
+
+ public override string ToString() {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "RequestUri = {0}", RequestUri));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ResponseUri = {0}", FinalUri));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "StatusCode = {0}", StatusCode));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentType = {0}", ContentType));
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "ContentEncoding = {0}", ContentEncoding));
+ sb.AppendLine("Headers:");
+ foreach (string header in Headers) {
+ sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "\t{0}: {1}", header, Headers[header]));
+ }
+ sb.AppendLine("Response:");
+ sb.AppendLine(ReadResponseString());
+ return sb.ToString();
+ }
}
}
diff --git a/src/DotNetOpenId/UriIdentifier.cs b/src/DotNetOpenId/UriIdentifier.cs
index 17a7697..821b7db 100644
--- a/src/DotNetOpenId/UriIdentifier.cs
+++ b/src/DotNetOpenId/UriIdentifier.cs
@@ -1,40 +1,68 @@
using System;
using System.Collections.Generic;
-using System.Text;
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using System.Web.UI.HtmlControls;
+using System.Xml;
using DotNetOpenId.RelyingParty;
using DotNetOpenId.Yadis;
-using System.Collections.Specialized;
-using System.Web.UI.HtmlControls;
-using System.Text.RegularExpressions;
-using System.Diagnostics;
namespace DotNetOpenId {
- class UriIdentifier : Identifier {
+ /// <summary>
+ /// A URI style of OpenID Identifier.
+ /// </summary>
+ [Serializable]
+ public sealed class UriIdentifier : Identifier {
static readonly string[] allowedSchemes = { "http", "https" };
+ /// <summary>
+ /// Converts a <see cref="UriIdentifier"/> instance to a <see cref="Uri"/> instance.
+ /// </summary>
public static implicit operator Uri(UriIdentifier identifier) {
if (identifier == null) return null;
return identifier.Uri;
}
+ /// <summary>
+ /// Converts a <see cref="Uri"/> instance to a <see cref="UriIdentifier"/> instance.
+ /// </summary>
public static implicit operator UriIdentifier(Uri identifier) {
if (identifier == null) return null;
return new UriIdentifier(identifier);
}
- public UriIdentifier(string uri) {
+ internal UriIdentifier(string uri) : this(uri, false) { }
+ internal UriIdentifier(string uri, bool requireSslDiscovery)
+ : base(requireSslDiscovery) {
if (string.IsNullOrEmpty(uri)) throw new ArgumentNullException("uri");
Uri canonicalUri;
- if (!TryCanonicalize(uri, out canonicalUri))
+ bool schemePrepended;
+ if (!TryCanonicalize(uri, out canonicalUri, requireSslDiscovery, out schemePrepended))
throw new UriFormatException();
+ if (requireSslDiscovery && canonicalUri.Scheme != Uri.UriSchemeHttps) {
+ throw new ArgumentException(Strings.ExplicitHttpUriSuppliedWithSslRequirement);
+ }
Uri = canonicalUri;
+ SchemeImplicitlyPrepended = schemePrepended;
}
- public UriIdentifier(Uri uri) {
+ internal UriIdentifier(Uri uri) : this(uri, false) { }
+ internal UriIdentifier(Uri uri, bool requireSslDiscovery)
+ : base(requireSslDiscovery) {
if (uri == null) throw new ArgumentNullException("uri");
if (!TryCanonicalize(new UriBuilder(uri), out uri))
throw new UriFormatException();
+ if (requireSslDiscovery && uri.Scheme != Uri.UriSchemeHttps) {
+ throw new ArgumentException(Strings.ExplicitHttpUriSuppliedWithSslRequirement);
+ }
Uri = uri;
+ SchemeImplicitlyPrepended = false;
}
- public Uri Uri { get; private set; }
+ internal Uri Uri { get; private set; }
+ /// <summary>
+ /// Gets whether the scheme was missing when this Identifier was
+ /// created and added automatically as part of the normalization
+ /// process.
+ /// </summary>
+ internal bool SchemeImplicitlyPrepended { get; private set; }
static bool isAllowedScheme(string uri) {
if (string.IsNullOrEmpty(uri)) return false;
@@ -43,16 +71,27 @@ namespace DotNetOpenId {
}
static bool isAllowedScheme(Uri uri) {
if (uri == null) return false;
- return Array.FindIndex(allowedSchemes, s =>
+ return Array.FindIndex(allowedSchemes, s =>
uri.Scheme.Equals(s, StringComparison.OrdinalIgnoreCase)) >= 0;
}
- static bool TryCanonicalize(string uri, out Uri canonicalUri) {
+ static bool TryCanonicalize(string uri, out Uri canonicalUri, bool forceHttpsDefaultScheme, out bool schemePrepended) {
+ if (string.IsNullOrEmpty(uri)) {
+ canonicalUri = null;
+ schemePrepended = false;
+ return false;
+ }
+
+ uri = uri.Trim();
canonicalUri = null;
+ schemePrepended = false;
try {
// Assume http:// scheme if an allowed scheme isn't given, and strip
// fragments off. Consistent with spec section 7.2#3
- if (!isAllowedScheme(uri)) uri = "http" + Uri.SchemeDelimiter + uri;
- if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute)) return false;
+ if (!isAllowedScheme(uri)) {
+ uri = (forceHttpsDefaultScheme ? Uri.UriSchemeHttps : Uri.UriSchemeHttp) +
+ Uri.SchemeDelimiter + uri;
+ schemePrepended = true;
+ }
// Use a UriBuilder because it helps to normalize the URL as well.
return TryCanonicalize(new UriBuilder(uri), out canonicalUri);
} catch (UriFormatException) {
@@ -68,16 +107,25 @@ namespace DotNetOpenId {
return result;
}
#endif
+ /// <summary>
+ /// Removes the fragment from a URL and sets the host to lowercase.
+ /// </summary>
+ /// <remarks>
+ /// This does NOT standardize an OpenID URL for storage in a database, as
+ /// it does nothing to convert the URL to a Claimed Identifier, besides the fact
+ /// that it only deals with URLs whereas OpenID 2.0 supports XRIs.
+ /// For this, you should lookup the value stored in IAuthenticationResponse.ClaimedIdentifier.
+ /// </remarks>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
static bool TryCanonicalize(UriBuilder uriBuilder, out Uri canonicalUri) {
uriBuilder.Host = uriBuilder.Host.ToLowerInvariant();
- uriBuilder.Fragment = null;
canonicalUri = uriBuilder.Uri;
return true;
}
internal static bool IsValidUri(string uri) {
Uri normalized;
- return TryCanonicalize(uri, out normalized);
+ bool schemePrepended;
+ return TryCanonicalize(uri, out normalized, false, out schemePrepended);
}
internal static bool IsValidUri(Uri uri) {
if (uri == null) return false;
@@ -96,79 +144,145 @@ namespace DotNetOpenId {
/// </param>
/// <param name="html">The HTML that was downloaded and should be searched.</param>
/// <returns>
- /// An initialized ServiceEndpoint if the OpenID Provider information was
- /// found. Otherwise null.
+ /// A sequence of any discovered ServiceEndpoints.
/// </returns>
- /// <remarks>
- /// OpenID 2.0 tags are always used if they are present, otherwise
- /// OpenID 1.x tags are used if present.
- /// </remarks>
- protected virtual ServiceEndpoint DiscoverFromHtml(Uri claimedIdentifier, string html) {
- Uri providerEndpoint = null;
- Protocol discoveredProtocol = null;
- Identifier providerLocalIdentifier = null;
+ private static IEnumerable<ServiceEndpoint> DiscoverFromHtml(Uri claimedIdentifier, string html) {
var linkTags = new List<HtmlLink>(Yadis.HtmlParser.HeadTags<HtmlLink>(html));
foreach (var protocol in Protocol.AllVersions) {
- foreach (var linkTag in linkTags) {
- // rel attributes are supposed to be interpreted with case INsensitivity,
- // and is a space-delimited list of values. (http://www.htmlhelp.com/reference/html40/values.html#linktypes)
- if (Regex.IsMatch(linkTag.Attributes["rel"], @"\b" + Regex.Escape(protocol.HtmlDiscoveryProviderKey) + @"\b", RegexOptions.IgnoreCase)) {
- if (Uri.TryCreate(linkTag.Href, UriKind.Absolute, out providerEndpoint)) {
- discoveredProtocol = protocol;
- break;
+ if (protocol.Equals(Protocol.v10)) {
+ // For HTML discovery, this is redunant with v1.1, so skip.
+ continue;
+ }
+
+ // rel attributes are supposed to be interpreted with case INsensitivity,
+ // and is a space-delimited list of values. (http://www.htmlhelp.com/reference/html40/values.html#linktypes)
+ var serverLinkTag = Util.FirstOrDefault(linkTags, tag => tag.Attributes["rel"] != null && Regex.IsMatch(tag.Attributes["rel"], @"\b" + Regex.Escape(protocol.HtmlDiscoveryProviderKey) + @"\b", RegexOptions.IgnoreCase));
+ if (serverLinkTag == null) {
+ continue;
+ }
+
+ Uri providerEndpoint = null;
+ if (Uri.TryCreate(serverLinkTag.Href, UriKind.Absolute, out providerEndpoint)) {
+ // See if a LocalId tag of the discovered version exists
+ Identifier providerLocalIdentifier = null;
+ var delegateLinkTag = Util.FirstOrDefault(linkTags, tag => tag.Attributes["rel"] != null && Regex.IsMatch(tag.Attributes["rel"], @"\b" + Regex.Escape(protocol.HtmlDiscoveryLocalIdKey) + @"\b", RegexOptions.IgnoreCase));
+ if (delegateLinkTag != null) {
+ if (Identifier.IsValid(delegateLinkTag.Href)) {
+ providerLocalIdentifier = delegateLinkTag.Href;
+ } else {
+ Logger.WarnFormat("Skipping endpoint data because local id is badly formed ({0}).", delegateLinkTag.Href);
+ continue; // skip to next version
}
}
+
+ // Choose the TypeURI to match the OpenID version detected.
+ string[] typeURIs = { protocol.ClaimedIdentifierServiceTypeURI };
+ yield return ServiceEndpoint.CreateForClaimedIdentifier(claimedIdentifier, providerLocalIdentifier,
+ providerEndpoint, typeURIs, (int?)null, (int?)null);
}
- if (providerEndpoint != null) break;
}
- if (providerEndpoint == null)
- return null; // html did not contain openid.server link
- // See if a LocalId tag of the discovered version exists
- foreach (var linkTag in linkTags) {
- if (Regex.IsMatch(linkTag.Attributes["rel"], @"\b" + Regex.Escape(discoveredProtocol.HtmlDiscoveryLocalIdKey) + @"\b", RegexOptions.IgnoreCase)) {
- if (Identifier.IsValid(linkTag.Href)) {
- providerLocalIdentifier = linkTag.Href;
- break;
+ }
+
+ internal override IEnumerable<ServiceEndpoint> Discover() {
+ List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();
+ // Attempt YADIS discovery
+ DiscoveryResult yadisResult = Yadis.Yadis.Discover(this, IsDiscoverySecureEndToEnd);
+ if (yadisResult != null) {
+ if (yadisResult.IsXrds) {
+ try {
+ XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
+ var xrdsEndpoints = xrds.CreateServiceEndpoints(yadisResult.NormalizedUri);
+ // Filter out insecure endpoints if high security is required.
+ if (IsDiscoverySecureEndToEnd) {
+ xrdsEndpoints = Util.Where(xrdsEndpoints, se => se.IsSecure);
+ }
+ endpoints.AddRange(xrdsEndpoints);
+ } catch (XmlException ex) {
+ Logger.Error("Error while parsing the XRDS document. Falling back to HTML discovery.", ex);
+ }
+ }
+ // Failing YADIS discovery of an XRDS document, we try HTML discovery.
+ if (endpoints.Count == 0) {
+ var htmlEndpoints = new List<ServiceEndpoint>(DiscoverFromHtml(yadisResult.NormalizedUri, yadisResult.ResponseText));
+ if (htmlEndpoints.Count > 0) {
+ Logger.DebugFormat("Total services discovered in HTML: {0}", htmlEndpoints.Count);
+ Logger.Debug(Util.ToString(htmlEndpoints, true));
+ endpoints.AddRange(Util.Where(htmlEndpoints, ep => !IsDiscoverySecureEndToEnd || ep.IsSecure));
+ if (endpoints.Count == 0) {
+ Logger.Info("No HTML discovered endpoints met the security requirements.");
+ }
} else {
- if (TraceUtil.Switch.TraceWarning)
- Trace.TraceWarning("Skipping endpoint data because local id is badly formed ({0}).", linkTag.Href);
- return null; // badly formed URL used as LocalId
+ Logger.Debug("HTML discovery failed to find any endpoints.");
}
+ } else {
+ Logger.Debug("Skipping HTML discovery because XRDS contained service endpoints.");
}
}
+ return endpoints;
+ }
- // Choose the TypeURI to match the OpenID version detected.
- string[] typeURIs = { discoveredProtocol.ClaimedIdentifierServiceTypeURI };
- return new ServiceEndpoint(claimedIdentifier, providerEndpoint,
- providerLocalIdentifier, typeURIs);
+ internal override Identifier TrimFragment() {
+ // If there is no fragment, we have no need to rebuild the Identifier.
+ if (Uri.Fragment == null || Uri.Fragment.Length == 0)
+ return this;
+
+ // Strip the fragment.
+ UriBuilder builder = new UriBuilder(Uri);
+ builder.Fragment = null;
+ return builder.Uri;
}
- internal override ServiceEndpoint Discover() {
- // Attempt YADIS discovery
- DiscoveryResult yadisResult = Yadis.Yadis.Discover(this);
- if (yadisResult != null) {
- if (yadisResult.IsXrds) {
- XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
- ServiceEndpoint ep = xrds.CreateServiceEndpoint(yadisResult.NormalizedUri);
- if (ep != null) return ep;
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ // If this Identifier is already secure, reuse it.
+ if (IsDiscoverySecureEndToEnd) {
+ secureIdentifier = this;
+ return true;
+ }
+
+ // If this identifier already uses SSL for initial discovery, return one
+ // that guarantees it will be used throughout the discovery process.
+ if (String.Equals(Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ secureIdentifier = new UriIdentifier(this.Uri, true);
+ return true;
+ }
+
+ // Otherwise, try to make this Identifier secure by normalizing to HTTPS instead of HTTP.
+ if (SchemeImplicitlyPrepended) {
+ UriBuilder newIdentifierUri = new UriBuilder(this.Uri);
+ newIdentifierUri.Scheme = Uri.UriSchemeHttps;
+ if (newIdentifierUri.Port == 80) {
+ newIdentifierUri.Port = 443;
}
- // Failing YADIS discovery of an XRDS document, we try HTML discovery.
- return DiscoverFromHtml(yadisResult.NormalizedUri, yadisResult.ResponseText);
+ secureIdentifier = new UriIdentifier(newIdentifierUri.Uri, true);
+ return true;
}
- return null;
+
+ // This identifier is explicitly NOT https, so we cannot change it.
+ secureIdentifier = new NoDiscoveryIdentifier(this, true);
+ return false;
}
+ /// <summary>
+ /// Tests equality between this URI and another URI.
+ /// </summary>
public override bool Equals(object obj) {
UriIdentifier other = obj as UriIdentifier;
if (other == null) return false;
return this.Uri == other.Uri;
}
+
+ /// <summary>
+ /// Returns the hash code of this XRI.
+ /// </summary>
public override int GetHashCode() {
return Uri.GetHashCode();
}
+
+ /// <summary>
+ /// Returns the string form of the URI.
+ /// </summary>
public override string ToString() {
return Uri.AbsoluteUri;
}
-
}
}
diff --git a/src/DotNetOpenId/Util.cs b/src/DotNetOpenId/Util.cs
index 65bf3b0..af00898 100644
--- a/src/DotNetOpenId/Util.cs
+++ b/src/DotNetOpenId/Util.cs
@@ -1,11 +1,14 @@
using System;
-using System.Collections.Specialized;
using System.Collections.Generic;
-using System.Text;
-using System.Web;
-using System.Globalization;
+using System.Collections.Specialized;
using System.Diagnostics;
+using System.Globalization;
+using System.Net;
+using System.Reflection;
+using System.Text;
using System.Text.RegularExpressions;
+using System.Web;
+using System.Web.UI;
namespace DotNetOpenId {
internal static class UriUtil {
@@ -49,6 +52,10 @@ namespace DotNetOpenId {
/// If null, <paramref name="builder"/> is not changed.
/// </param>
public static void AppendQueryArgs(UriBuilder builder, IDictionary<string, string> args) {
+ if (builder == null) {
+ throw new ArgumentNullException("builder");
+ }
+
if (args != null && args.Count > 0) {
StringBuilder sb = new StringBuilder(50 + args.Count * 10);
if (!string.IsNullOrEmpty(builder.Query)) {
@@ -62,6 +69,30 @@ namespace DotNetOpenId {
}
/// <summary>
+ /// Adds parameters to a query string, replacing parameters that
+ /// match ones that already exist in the query string.
+ /// </summary>
+ /// <param name="builder">The UriBuilder to add arguments to.</param>
+ /// <param name="args">
+ /// The arguments to add to the query.
+ /// If null, <paramref name="builder"/> is not changed.
+ /// </param>
+ internal static void AppendAndReplaceQueryArgs(UriBuilder builder, IDictionary<string, string> args) {
+ if (builder == null) {
+ throw new ArgumentNullException("builder");
+ }
+
+ if (args != null && args.Count > 0) {
+ NameValueCollection aggregatedArgs = HttpUtility.ParseQueryString(builder.Query);
+ foreach (var pair in args) {
+ aggregatedArgs[pair.Key] = pair.Value;
+ }
+
+ builder.Query = CreateQueryString(aggregatedArgs);
+ }
+ }
+
+ /// <summary>
/// Equivalent to UriBuilder.ToString() but omits port # if it may be implied.
/// Equivalent to UriBuilder.Uri.ToString(), but doesn't throw an exception if the Host has a wildcard.
/// </summary>
@@ -87,6 +118,15 @@ namespace DotNetOpenId {
internal static class Util {
internal const string DefaultNamespace = "DotNetOpenId";
+ public static string DotNetOpenIdVersion {
+ get {
+ string assemblyFullName = Assembly.GetExecutingAssembly().FullName;
+ bool official = assemblyFullName.Contains("PublicKeyToken=2780ccd10d57b246");
+ // We use InvariantCulture since this is used for logging.
+ return string.Format(CultureInfo.InvariantCulture, "{0} ({1})", assemblyFullName, official ? "official" : "private");
+ }
+ }
+
public static IDictionary<string, string> NameValueCollectionToDictionary(NameValueCollection nvc) {
if (nvc == null) return null;
var dict = new Dictionary<string, string>(nvc.Count);
@@ -111,19 +151,74 @@ namespace DotNetOpenId {
return nvc;
}
- public static IDictionary<string, string> GetQueryFromContext() {
+ /// <summary>
+ /// Gets the query data from the original request (before any URL rewriting has occurred.)
+ /// </summary>
+ public static NameValueCollection GetQueryFromContextNVC() {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
- var query = HttpContext.Current.Request.RequestType == "GET" ?
- HttpContext.Current.Request.QueryString : HttpContext.Current.Request.Form;
- return NameValueCollectionToDictionary(query);
+ HttpRequest request = HttpContext.Current.Request;
+ // This request URL may have been rewritten by the host site.
+ // For openid protocol purposes, we really need to look at
+ // the original query parameters before any rewriting took place.
+ if (request.Url.PathAndQuery == request.RawUrl) {
+ // No rewriting has taken place.
+ return request.QueryString;
+ } else {
+ // Rewriting detected! Recover the original request URI.
+ return HttpUtility.ParseQueryString(GetRequestUrlFromContext().Query);
+ }
}
- internal static Uri GetRequestUrlFromContext() {
+ /// <summary>
+ /// Gets the query or form data from the original request (before any URL rewriting has occurred.)
+ /// </summary>
+ public static NameValueCollection GetQueryOrFormFromContextNVC() {
if (HttpContext.Current == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
- UriBuilder builder = new UriBuilder(HttpContext.Current.Request.Url);
- // If a cookieless session is in use, the Request.Url will not include the session bit,
- // so we add it here ourselves.
- builder.Path = HttpContext.Current.Response.ApplyAppPathModifier(builder.Path);
- return builder.Uri;
+ HttpRequest request = HttpContext.Current.Request;
+ NameValueCollection query;
+ if (request.RequestType == "GET") {
+ query = GetQueryFromContextNVC();
+ } else {
+ query = request.Form;
+ }
+ return query;
+ }
+ /// <summary>
+ /// Gets the querystring or form data from the original request (before any URL rewriting has occurred.)
+ /// </summary>
+ public static IDictionary<string, string> GetQueryOrFormFromContext() {
+ return NameValueCollectionToDictionary(GetQueryOrFormFromContextNVC());
+ }
+ /// <summary>
+ /// Gets the original request URL, as seen from the browser before any URL rewrites on the server if any.
+ /// Cookieless session directory (if applicable) is also included.
+ /// </summary>
+ internal static Uri GetRequestUrlFromContext() {
+ HttpContext context = HttpContext.Current;
+ if (context == null) throw new InvalidOperationException(Strings.CurrentHttpContextRequired);
+ // We use Request.Url for the full path to the server, and modify it
+ // with Request.RawUrl to capture both the cookieless session "directory" if it exists
+ // and the original path in case URL rewriting is going on. We don't want to be
+ // fooled by URL rewriting because we're comparing the actual URL with what's in
+ // the return_to parameter in some cases.
+ return new Uri(context.Request.Url, context.Request.RawUrl);
+ // Response.ApplyAppPathModifier(builder.Path) would have worked for the cookieless
+ // session, but not the URL rewriting problem.
+ }
+
+ public static void ApplyHeadersToResponse(WebHeaderCollection headers, HttpResponse response) {
+ if (headers == null) throw new ArgumentNullException("headers");
+ if (response == null) throw new ArgumentNullException("response");
+ foreach (string headerName in headers) {
+ switch (headerName) {
+ case "Content-Type":
+ response.ContentType = headers[HttpResponseHeader.ContentType];
+ break;
+ // Add more special cases here as necessary.
+ default:
+ response.AddHeader(headerName, headers[headerName]);
+ break;
+ }
+ }
}
public static string GetRequiredArg(IDictionary<string, string> query, string key) {
@@ -135,6 +230,15 @@ namespace DotNetOpenId {
Strings.MissingOpenIdQueryParameter, key), query);
return value;
}
+ public static string GetRequiredArgAllowEmptyValue(IDictionary<string, string> query, string key) {
+ if (query == null) throw new ArgumentNullException("query");
+ if (key == null) throw new ArgumentNullException("key");
+ string value;
+ if (!query.TryGetValue(key, out value))
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.MissingOpenIdQueryParameter, key), query);
+ return value;
+ }
public static string GetOptionalArg(IDictionary<string, string> query, string key) {
if (query == null) throw new ArgumentNullException("query");
if (key == null) throw new ArgumentNullException("key");
@@ -183,13 +287,46 @@ namespace DotNetOpenId {
}
public static Realm GetOptionalRealmArg(IDictionary<string, string> query, string key) {
try {
- return Util.GetOptionalArg(query, key);
+ string realm = Util.GetOptionalArg(query, key);
+ // Take care to not return the empty string in case the RP
+ // sent us realm= but didn't provide a value.
+ return realm.Length > 0 ? realm : null;
} catch (UriFormatException ex) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue, key,
- Util.GetOptionalArg(query, key)), ex);
+ Util.GetOptionalArg(query, key)), null, query, ex);
+ }
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", MessageId = "DotNetOpenId.Realm")]
+ internal static UriBuilder GetResolvedRealm(Page page, 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(Util.GetRequestUrlFromContext(), page.ResolveUrl(realmNoWildcard)));
+
+ if (foundWildcard) {
+ fullyQualifiedRealm.Host = "*." + fullyQualifiedRealm.Host;
}
+
+ // Is it valid?
+ new Realm(fullyQualifiedRealm); // throws if not valid
+
+ return fullyQualifiedRealm;
}
+
public static bool ArrayEquals<T>(T[] first, T[] second) {
if (first == null) throw new ArgumentNullException("first");
if (second == null) throw new ArgumentNullException("second");
@@ -199,6 +336,43 @@ namespace DotNetOpenId {
return true;
}
+ // The characters to escape here are inspired by
+ // http://code.google.com/p/doctype/wiki/ArticleXSSInJavaScript
+ static readonly Dictionary<string, string> javascriptStaticStringEscaping = new Dictionary<string,string> {
+ {"\\", @"\\" }, // this WAS just above the & substitution but we moved it here to prevent double-escaping
+ {"\t", @"\t" },
+ {"\n", @"\n" },
+ {"\r", @"\r" },
+ {"\u0085", @"\u0085" },
+ {"\u2028", @"\u2028" },
+ {"\u2029", @"\u2029" },
+ {"'", @"\x27" },
+ {"\"", @"\x22" },
+ {"&", @"\x26" },
+ {"<", @"\x3c" },
+ {">", @"\x3e" },
+ {"=", @"\x3d" },
+ };
+
+ /// <summary>
+ /// Prepares what SHOULD be simply a string value for safe injection into Javascript
+ /// by using appropriate character escaping.
+ /// </summary>
+ /// <param name="value">The untrusted string value to be escaped to protected against XSS attacks.</param>
+ /// <returns>The escaped string.</returns>
+ public static string GetSafeJavascriptValue(string value) {
+ if (value == null) return "null";
+ // We use a StringBuilder because we have potentially many replacements to do,
+ // and we don't want to create a new string for every intermediate replacement step.
+ StringBuilder builder = new StringBuilder(value);
+ foreach (var pair in javascriptStaticStringEscaping) {
+ builder.Replace(pair.Key, pair.Value);
+ }
+ builder.Insert(0, '\'');
+ builder.Append('\'');
+ return builder.ToString();
+ }
+
internal delegate R Func<T, R>(T t);
/// <summary>
/// Scans a list for matches with some element of the OpenID protocol,
@@ -217,5 +391,115 @@ namespace DotNetOpenId {
}
return null;
}
+
+ internal static T FirstOrDefault<T>(IEnumerable<T> sequence, Func<T, bool> predicate) {
+ IEnumerator<T> iterator = Where(sequence, predicate).GetEnumerator();
+ return iterator.MoveNext() ? iterator.Current : default(T);
+ }
+ internal static IEnumerable<T> Where<T>(IEnumerable<T> sequence, Func<T, bool> predicate) {
+ foreach (T item in sequence) {
+ if (predicate(item)) {
+ yield return item;
+ }
+ }
+ }
+ /// <summary>
+ /// Tests two sequences for same contents and ordering.
+ /// </summary>
+ internal static bool AreSequencesEquivalent<T>(IEnumerable<T> sequence1, IEnumerable<T> sequence2) {
+ if (sequence1 == null && sequence2 == null) return true;
+ if (sequence1 == null) throw new ArgumentNullException("sequence1");
+ if (sequence2 == null) throw new ArgumentNullException("sequence2");
+
+ IEnumerator<T> iterator1 = sequence1.GetEnumerator();
+ IEnumerator<T> iterator2 = sequence2.GetEnumerator();
+ bool movenext1 , movenext2;
+ while (true) {
+ movenext1 = iterator1.MoveNext();
+ movenext2 = iterator2.MoveNext();
+ if (!movenext1 || !movenext2) break; // if we've reached the end of at least one sequence
+ object obj1 = iterator1.Current;
+ object obj2 = iterator2.Current;
+ if (obj1 == null && obj2 == null) continue; // both null is ok
+ if (obj1 == null ^ obj2 == null) return false; // exactly one null is different
+ if (!obj1.Equals(obj2)) return false; // if they're not equal to each other
+ }
+
+ return movenext1 == movenext2; // did they both reach the end together?
+ }
+
+ /// <summary>
+ /// Prepares a dictionary for printing as a string.
+ /// </summary>
+ /// <remarks>
+ /// The work isn't done until (and if) the
+ /// <see cref="Object.ToString"/> method is actually called, which makes it great
+ /// for logging complex objects without being in a conditional block.
+ /// </remarks>
+ internal static object ToString<K, V>(IEnumerable<KeyValuePair<K, V>> pairs) {
+ return new DelayedToString<IEnumerable<KeyValuePair<K, V>>>(pairs, p => {
+ var dictionary = pairs as IDictionary<K, V>;
+ StringBuilder sb = new StringBuilder(dictionary != null ? dictionary.Count * 40 : 200);
+ foreach (var pair in pairs) {
+ sb.AppendFormat("\t{0}: {1}{2}", pair.Key, pair.Value, Environment.NewLine);
+ }
+ return sb.ToString();
+ });
+ }
+ internal static object ToString<T>(IEnumerable<T> list) {
+ return ToString<T>(list, false);
+ }
+ internal static object ToString<T>(IEnumerable<T> list, bool multiLineElements) {
+ return new DelayedToString<IEnumerable<T>>(list, l => {
+ StringBuilder sb = new StringBuilder();
+ if (multiLineElements) {
+ sb.AppendLine("[{");
+ foreach (T obj in l) {
+ // Prepare the string repersentation of the object
+ string objString = obj != null ? obj.ToString() : "<NULL>";
+
+ // Indent every line printed
+ objString = objString.Replace(Environment.NewLine, Environment.NewLine + "\t");
+ sb.Append("\t");
+ sb.Append(objString);
+
+ if (!objString.EndsWith(Environment.NewLine)) {
+ sb.AppendLine();
+ }
+ sb.AppendLine("}, {");
+ }
+ if (sb.Length > 2) { // if anything was in the enumeration
+ sb.Length -= 2 + Environment.NewLine.Length; // trim off the last ", {\r\n"
+ } else {
+ sb.Length -= 1; // trim off the opening {
+ }
+ sb.Append("]");
+ return sb.ToString();
+ } else {
+ sb.Append("{");
+ foreach (T obj in l) {
+ sb.Append(obj != null ? obj.ToString() : "<NULL>");
+ sb.AppendLine(",");
+ }
+ if (sb.Length > 1) {
+ sb.Length -= 1;
+ }
+ sb.Append("}");
+ return sb.ToString();
+ }
+ });
+ }
+
+ private class DelayedToString<T> {
+ public DelayedToString(T obj, Func<T, string> toString) {
+ this.obj = obj;
+ this.toString = toString;
+ }
+ T obj;
+ Func<T, string> toString;
+ public override string ToString() {
+ return toString(obj);
+ }
+ }
}
}
diff --git a/src/DotNetOpenId/XrdsPublisher.cs b/src/DotNetOpenId/XrdsPublisher.cs
index 8f610ab..2c56cd4 100644
--- a/src/DotNetOpenId/XrdsPublisher.cs
+++ b/src/DotNetOpenId/XrdsPublisher.cs
@@ -135,13 +135,13 @@ namespace DotNetOpenId {
if (Enabled && Visible && !string.IsNullOrEmpty(XrdsUrl)) {
if ((XrdsAdvertisement & XrdsUrlLocations.HttpHeader) != 0) {
Page.Response.AddHeader(Yadis.Yadis.HeaderName,
- new Uri(Page.Request.Url, Page.Response.ApplyAppPathModifier(XrdsUrl)).AbsoluteUri);
+ new Uri(Util.GetRequestUrlFromContext(), Page.Response.ApplyAppPathModifier(XrdsUrl)).AbsoluteUri);
}
if ((XrdsAdvertisement & XrdsUrlLocations.HtmlMeta) != 0) {
writer.WriteBeginTag("meta");
writer.WriteAttribute("http-equiv", Yadis.Yadis.HeaderName);
writer.WriteAttribute("content",
- new Uri(Page.Request.Url, Page.Response.ApplyAppPathModifier(XrdsUrl)).AbsoluteUri);
+ new Uri(Util.GetRequestUrlFromContext(), Page.Response.ApplyAppPathModifier(XrdsUrl)).AbsoluteUri);
writer.Write("/>");
writer.WriteLine();
}
diff --git a/src/DotNetOpenId/XriIdentifier.cs b/src/DotNetOpenId/XriIdentifier.cs
index a82913d..129f8fa 100644
--- a/src/DotNetOpenId/XriIdentifier.cs
+++ b/src/DotNetOpenId/XriIdentifier.cs
@@ -8,14 +8,26 @@ using System.IO;
using System.Xml;
namespace DotNetOpenId {
- class XriIdentifier : Identifier {
+ /// <summary>
+ /// An XRI style of OpenID Identifier.
+ /// </summary>
+ [Serializable]
+ public sealed class XriIdentifier : Identifier {
internal static readonly char[] GlobalContextSymbols = { '=', '@', '+', '$', '!' };
const string xriScheme = "xri://";
- public XriIdentifier(string xri) {
+ internal XriIdentifier(string xri) : this(xri, false) { }
+ internal XriIdentifier(string xri, bool requireSsl)
+ : base(requireSsl) {
if (!IsValidXri(xri))
throw new FormatException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidXri, xri));
+ xriResolverProxy = xriResolverProxyTemplate;
+ if (requireSsl) {
+ // Indicate to xri.net that we require SSL to be used for delegated resolution
+ // of community i-names.
+ xriResolverProxy += ";https=true";
+ }
OriginalXri = xri;
CanonicalXri = canonicalizeXri(xri);
}
@@ -23,17 +35,18 @@ namespace DotNetOpenId {
/// <summary>
/// The original XRI supplied to the constructor.
/// </summary>
- public string OriginalXri { get; private set; }
+ internal string OriginalXri { get; private set; }
/// <summary>
/// The canonical form of the XRI string.
/// </summary>
- public string CanonicalXri { get; private set; }
+ internal string CanonicalXri { get; private set; }
/// <summary>
/// Tests whether a given string represents a valid XRI format.
/// </summary>
internal static bool IsValidXri(string xri) {
if (string.IsNullOrEmpty(xri)) throw new ArgumentNullException("xri");
+ xri = xri.Trim();
// TODO: better validation code here
return xri.IndexOfAny(GlobalContextSymbols) == 0
|| xri.StartsWith("(", StringComparison.Ordinal)
@@ -44,40 +57,83 @@ namespace DotNetOpenId {
/// Takes any valid form of XRI string and returns the canonical form of the same XRI.
/// </summary>
static string canonicalizeXri(string xri) {
+ xri = xri.Trim();
if (xri.StartsWith(xriScheme, StringComparison.OrdinalIgnoreCase))
xri = xri.Substring(xriScheme.Length);
return xri;
}
- const string xriResolverProxy = "http://xri.net/{0}?_xrd_r=application/xrds%2Bxml;sep=false";
+ /// <summary>
+ /// The magic URL that will provide us an XRDS document for a given XRI identifier.
+ /// </summary>
+ /// <remarks>
+ /// We use application/xrd+xml instead of application/xrds+xml because it gets
+ /// xri.net to automatically give us exactly the right XRD element for community i-names
+ /// automatically, saving us having to choose which one to use out of the result.
+ /// The ssl=true parameter tells the proxy resolver to accept only SSL connections
+ /// when resolving community i-names.
+ /// </remarks>
+ const string xriResolverProxyTemplate = "https://xri.net/{0}?_xrd_r=application/xrd%2Bxml;sep=false";
+ readonly string xriResolverProxy;
/// <summary>
/// Resolves the XRI to a URL from which an XRDS document may be downloaded.
/// </summary>
- protected virtual Uri XrdsUrl {
+ private Uri XrdsUrl {
get {
- return new Uri(string.Format(CultureInfo.InvariantCulture,
+ return new Uri(string.Format(CultureInfo.InvariantCulture,
xriResolverProxy, this));
}
}
XrdsDocument downloadXrds() {
var xrdsResponse = UntrustedWebRequest.Request(XrdsUrl);
- return new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream));
+ XrdsDocument doc = new XrdsDocument(XmlReader.Create(xrdsResponse.ResponseStream));
+ if (!doc.IsXrdResolutionSuccessful) {
+ throw new OpenIdException(Strings.XriResolutionFailed);
+ }
+ return doc;
+ }
+
+ internal override IEnumerable<ServiceEndpoint> Discover() {
+ return downloadXrds().CreateServiceEndpoints(this);
+ }
+
+ /// <summary>
+ /// Performs discovery on THIS identifier, but generates <see cref="ServiceEndpoint"/>
+ /// instances that treat another given identifier as the user-supplied identifier.
+ /// </summary>
+ internal IEnumerable<ServiceEndpoint> Discover(XriIdentifier userSuppliedIdentifier) {
+ return downloadXrds().CreateServiceEndpoints(userSuppliedIdentifier);
}
- internal override ServiceEndpoint Discover() {
- return downloadXrds().CreateServiceEndpoint(this);
+ internal override Identifier TrimFragment() {
+ return this;
}
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ secureIdentifier = IsDiscoverySecureEndToEnd ? this : new XriIdentifier(this, true);
+ return true;
+ }
+
+ /// <summary>
+ /// Tests equality between this XRI and another XRI.
+ /// </summary>
public override bool Equals(object obj) {
XriIdentifier other = obj as XriIdentifier;
if (other == null) return false;
return this.CanonicalXri == other.CanonicalXri;
}
+
+ /// <summary>
+ /// Returns the hash code of this XRI.
+ /// </summary>
public override int GetHashCode() {
return CanonicalXri.GetHashCode();
}
+ /// <summary>
+ /// Returns the canonical string form of the XRI.
+ /// </summary>
public override string ToString() {
return CanonicalXri;
}
diff --git a/src/DotNetOpenId/Yadis/ContentTypes.cs b/src/DotNetOpenId/Yadis/ContentTypes.cs
index c2a653a..4f4dd37 100644
--- a/src/DotNetOpenId/Yadis/ContentTypes.cs
+++ b/src/DotNetOpenId/Yadis/ContentTypes.cs
@@ -7,5 +7,6 @@ namespace DotNetOpenId.Yadis {
public const string Html = "text/html";
public const string XHtml = "application/xhtml+xml";
public const string Xrds = "application/xrds+xml";
+ public const string Xml = "text/xml";
}
}
diff --git a/src/DotNetOpenId/Yadis/ServiceElement.cs b/src/DotNetOpenId/Yadis/ServiceElement.cs
index 3fce480..21837e1 100644
--- a/src/DotNetOpenId/Yadis/ServiceElement.cs
+++ b/src/DotNetOpenId/Yadis/ServiceElement.cs
@@ -13,8 +13,11 @@ namespace DotNetOpenId.Yadis {
get { return (XrdElement)ParentNode; }
}
- public int Priority {
- get { return Node.SelectSingleNode("@priority", XmlNamespaceResolver).ValueAsInt; }
+ public int? Priority {
+ get {
+ XPathNavigator n = Node.SelectSingleNode("@priority", XmlNamespaceResolver);
+ return n != null ? n.ValueAsInt : (int?)null;
+ }
}
public IEnumerable<UriElement> UriElements {
@@ -41,7 +44,7 @@ namespace DotNetOpenId.Yadis {
XPathNodeIterator types = Node.Select("xrd:Type", XmlNamespaceResolver);
string[] typeUris = new string[types.Count];
int i = 0;
- foreach(XPathNavigator type in types) {
+ foreach (XPathNavigator type in types) {
typeUris[i++] = type.Value;
}
return typeUris;
@@ -50,16 +53,34 @@ namespace DotNetOpenId.Yadis {
public Identifier ProviderLocalIdentifier {
get {
- var n = Node.SelectSingleNode("xrd:LocalID", XmlNamespaceResolver)
+ var n = Node.SelectSingleNode("xrd:LocalID", XmlNamespaceResolver)
?? Node.SelectSingleNode("openid10:Delegate", XmlNamespaceResolver);
- return (n != null) ? n.Value : null;
+ if (n != null && n.Value != null) {
+ string value = n.Value.Trim();
+ if (value.Length > 0) {
+ return n.Value;
+ }
+ }
+
+ return null;
}
}
#region IComparable<ServiceElement> Members
public int CompareTo(ServiceElement other) {
- return Priority.CompareTo(other.Priority);
+ if (other == null) return -1;
+ if (Priority.HasValue && other.Priority.HasValue) {
+ return Priority.Value.CompareTo(other.Priority.Value);
+ } else {
+ if (Priority.HasValue) {
+ return -1;
+ } else if (other.Priority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
}
#endregion
diff --git a/src/DotNetOpenId/Yadis/UriElement.cs b/src/DotNetOpenId/Yadis/UriElement.cs
index 20f8fc4..27d965a 100644
--- a/src/DotNetOpenId/Yadis/UriElement.cs
+++ b/src/DotNetOpenId/Yadis/UriElement.cs
@@ -9,12 +9,24 @@ namespace DotNetOpenId.Yadis {
base(uriElement, service) {
}
- public int Priority {
- get { return Node.SelectSingleNode("@priority", XmlNamespaceResolver).ValueAsInt; }
+ public int? Priority {
+ get {
+ XPathNavigator n = Node.SelectSingleNode("@priority", XmlNamespaceResolver);
+ return n != null ? n.ValueAsInt : (int?)null;
+ }
}
public Uri Uri {
- get { return new Uri(Node.Value); }
+ get {
+ if (Node.Value != null) {
+ string value = Node.Value.Trim();
+ if (value.Length > 0) {
+ return new Uri(value);
+ }
+ }
+
+ return null;
+ }
}
public ServiceElement Service {
@@ -24,8 +36,21 @@ namespace DotNetOpenId.Yadis {
#region IComparable<UriElement> Members
public int CompareTo(UriElement other) {
+ if (other == null) return -1;
int compare = Service.CompareTo(other.Service);
- return compare != 0 ? compare : Priority.CompareTo(other.Priority);
+ if (compare != 0) return compare;
+
+ if (Priority.HasValue && other.Priority.HasValue) {
+ return Priority.Value.CompareTo(other.Priority.Value);
+ } else {
+ if (Priority.HasValue) {
+ return -1;
+ } else if (other.Priority.HasValue) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
}
#endregion
diff --git a/src/DotNetOpenId/Yadis/XrdElement.cs b/src/DotNetOpenId/Yadis/XrdElement.cs
index 285677b..5c0ba44 100644
--- a/src/DotNetOpenId/Yadis/XrdElement.cs
+++ b/src/DotNetOpenId/Yadis/XrdElement.cs
@@ -22,6 +22,28 @@ namespace DotNetOpenId.Yadis {
}
}
+
+ int XriResolutionStatusCode {
+ get {
+ var n = Node.SelectSingleNode("xrd:Status", XmlNamespaceResolver);
+ string codeString;
+ if (n == null || string.IsNullOrEmpty(codeString = n.GetAttribute("code", ""))) {
+ throw new OpenIdException(Strings.XriResolutionStatusMissing);
+ }
+ int code;
+ if (!int.TryParse(codeString, out code) || code < 100 || code > 399) {
+ throw new OpenIdException(Strings.XriResolutionStatusMissing);
+ }
+ return code;
+ }
+ }
+
+ public bool IsXriResolutionSuccessful {
+ get {
+ return XriResolutionStatusCode == 100;
+ }
+ }
+
public string CanonicalID {
get {
var n = Node.SelectSingleNode("xrd:CanonicalID", XmlNamespaceResolver);
@@ -29,6 +51,13 @@ namespace DotNetOpenId.Yadis {
}
}
+ public bool IsCanonicalIdVerified {
+ get {
+ var n = Node.SelectSingleNode("xrd:Status", XmlNamespaceResolver);
+ return n != null && string.Equals(n.GetAttribute("cid", ""), "verified", StringComparison.Ordinal);
+ }
+ }
+
IEnumerable<ServiceElement> searchForServiceTypeUris(Util.Func<Protocol, string> p) {
var xpath = new StringBuilder();
xpath.Append("xrd:Service[");
diff --git a/src/DotNetOpenId/Yadis/XrdsDocument.cs b/src/DotNetOpenId/Yadis/XrdsDocument.cs
index 2d2cadc..b963f08 100644
--- a/src/DotNetOpenId/Yadis/XrdsDocument.cs
+++ b/src/DotNetOpenId/Yadis/XrdsDocument.cs
@@ -1,10 +1,10 @@
-using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Xml.XPath;
-using System.Collections.Generic;
-using DotNetOpenId.RelyingParty;
using DotNetOpenId.Provider;
+using DotNetOpenId.RelyingParty;
namespace DotNetOpenId.Yadis {
class XrdsDocument : XrdsNode {
@@ -21,44 +21,91 @@ namespace DotNetOpenId.Yadis {
public IEnumerable<XrdElement> XrdElements {
get {
- foreach (XPathNavigator node in Node.Select("/xrds:XRDS/xrd:XRD", XmlNamespaceResolver)) {
- yield return new XrdElement(node, this);
+ // We may be looking at a full XRDS document (in the case of YADIS discovery)
+ // or we may be looking at just an individual XRD element from a larger document
+ // if we asked xri.net for just one.
+ if (Node.SelectSingleNode("/xrds:XRDS", XmlNamespaceResolver) != null) {
+ foreach (XPathNavigator node in Node.Select("/xrds:XRDS/xrd:XRD", XmlNamespaceResolver)) {
+ yield return new XrdElement(node, this);
+ }
+ } else {
+ XPathNavigator node = Node.SelectSingleNode("/xrd:XRD", XmlNamespaceResolver);
+ if (node != null) {
+ yield return new XrdElement(node, this);
+ }
}
}
}
- internal ServiceEndpoint CreateServiceEndpoint(UriIdentifier claimedIdentifier) {
- return createServiceEndpoint(claimedIdentifier);
+ internal IEnumerable<ServiceEndpoint> CreateServiceEndpoints(UriIdentifier claimedIdentifier) {
+ List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();
+ endpoints.AddRange(generateOPIdentifierServiceEndpoints(claimedIdentifier));
+ // If any OP Identifier service elements were found, we must not proceed
+ // to return any Claimed Identifier services.
+ if (endpoints.Count == 0) {
+ endpoints.AddRange(generateClaimedIdentifierServiceEndpoints(claimedIdentifier));
+ }
+ Logger.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Debug(Util.ToString(endpoints, true));
+ return endpoints;
}
- internal ServiceEndpoint CreateServiceEndpoint(XriIdentifier userSuppliedIdentifier) {
- return createServiceEndpoint(userSuppliedIdentifier);
+ internal IEnumerable<ServiceEndpoint> CreateServiceEndpoints(XriIdentifier userSuppliedIdentifier) {
+ List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();
+ endpoints.AddRange(generateOPIdentifierServiceEndpoints(userSuppliedIdentifier));
+ // If any OP Identifier service elements were found, we must not proceed
+ // to return any Claimed Identifier services.
+ if (endpoints.Count == 0) {
+ endpoints.AddRange(generateClaimedIdentifierServiceEndpoints(userSuppliedIdentifier));
+ }
+ Logger.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Debug(Util.ToString(endpoints, true));
+ return endpoints;
}
- ServiceEndpoint createServiceEndpoint(Identifier claimedIdentifier) {
- // First search for OP Identifier service elements
+ IEnumerable<ServiceEndpoint> generateOPIdentifierServiceEndpoints(Identifier opIdentifier) {
foreach (var service in findOPIdentifierServices()) {
foreach (var uri in service.UriElements) {
var protocol = Util.FindBestVersion(p => p.OPIdentifierServiceTypeURI, service.TypeElementUris);
- return new ServiceEndpoint(protocol.ClaimedIdentifierForOPIdentifier, uri.Uri,
- protocol.ClaimedIdentifierForOPIdentifier, service.TypeElementUris);
+ yield return ServiceEndpoint.CreateForProviderIdentifier(
+ opIdentifier, uri.Uri, service.TypeElementUris,
+ service.Priority, uri.Priority);
}
}
- // Since we could not find an OP Identifier service element,
- // search for a Claimed Identifier element.
+ }
+
+ IEnumerable<ServiceEndpoint> generateClaimedIdentifierServiceEndpoints(UriIdentifier claimedIdentifier) {
+ foreach (var service in findClaimedIdentifierServices()) {
+ foreach (var uri in service.UriElements) {
+ if (uri.Uri == null) {
+ continue;
+ }
+
+ yield return ServiceEndpoint.CreateForClaimedIdentifier(
+ claimedIdentifier, service.ProviderLocalIdentifier,
+ uri.Uri, service.TypeElementUris, service.Priority, uri.Priority);
+ }
+ }
+ }
+
+ IEnumerable<ServiceEndpoint> generateClaimedIdentifierServiceEndpoints(XriIdentifier userSuppliedIdentifier) {
foreach (var service in findClaimedIdentifierServices()) {
foreach (var uri in service.UriElements) {
// spec section 7.3.2.3 on Claimed Id -> CanonicalID substitution
- if (claimedIdentifier is XriIdentifier) {
- if (service.Xrd.CanonicalID == null)
- throw new OpenIdException(Strings.MissingCanonicalIDElement, claimedIdentifier);
- claimedIdentifier = service.Xrd.CanonicalID;
+ if (service.Xrd.CanonicalID == null) {
+ Logger.WarnFormat(Strings.MissingCanonicalIDElement, userSuppliedIdentifier);
+ break; // skip on to next service
+ }
+ if (!service.Xrd.IsCanonicalIdVerified) {
+ throw new OpenIdException(Strings.CIDVerificationFailed, userSuppliedIdentifier);
}
- return new ServiceEndpoint(claimedIdentifier, uri.Uri,
- service.ProviderLocalIdentifier, service.TypeElementUris);
+ // In the case of XRI names, the ClaimedId is actually the CanonicalID.
+ var claimedIdentifier = new XriIdentifier(service.Xrd.CanonicalID);
+ yield return ServiceEndpoint.CreateForClaimedIdentifier(
+ claimedIdentifier, userSuppliedIdentifier, service.ProviderLocalIdentifier,
+ uri.Uri, service.TypeElementUris, service.Priority, uri.Priority);
}
}
- return null;
}
internal IEnumerable<RelyingPartyReceivingEndpoint> FindRelyingPartyReceivingEndpoints() {
@@ -91,10 +138,21 @@ namespace DotNetOpenId.Yadis {
IEnumerable<ServiceElement> findReturnToServices() {
foreach (var xrd in XrdElements) {
- foreach( var service in xrd.OpenIdRelyingPartyReturnToServices) {
+ foreach (var service in xrd.OpenIdRelyingPartyReturnToServices) {
yield return service;
}
}
}
+
+ internal bool IsXrdResolutionSuccessful {
+ get {
+ foreach (var xrd in XrdElements) {
+ if (!xrd.IsXriResolutionSuccessful) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
}
}
diff --git a/src/DotNetOpenId/Yadis/XrdsNode.cs b/src/DotNetOpenId/Yadis/XrdsNode.cs
index c3e8f3f..5ad5379 100644
--- a/src/DotNetOpenId/Yadis/XrdsNode.cs
+++ b/src/DotNetOpenId/Yadis/XrdsNode.cs
@@ -10,11 +10,22 @@ namespace DotNetOpenId.Yadis {
internal const string XrdsNamespace = "xri://$xrds";
protected XrdsNode(XPathNavigator node, XrdsNode parentNode) {
+ if (node == null) {
+ throw new ArgumentNullException("node");
+ }
+ if (parentNode == null) {
+ throw new ArgumentNullException("parentNode");
+ }
+
Node = node;
ParentNode = parentNode;
XmlNamespaceResolver = ParentNode.XmlNamespaceResolver;
}
protected XrdsNode(XPathNavigator document) {
+ if (document == null) {
+ throw new ArgumentNullException("document");
+ }
+
Node = document;
XmlNamespaceResolver = new XmlNamespaceManager(document.NameTable);
}
diff --git a/src/DotNetOpenId/Yadis/Yadis.cs b/src/DotNetOpenId/Yadis/Yadis.cs
index b658cf6..37a58e4 100644
--- a/src/DotNetOpenId/Yadis/Yadis.cs
+++ b/src/DotNetOpenId/Yadis/Yadis.cs
@@ -7,37 +7,90 @@ using System.Xml;
using System.Xml.Serialization;
using System.Net.Mime;
using System.Web.UI.HtmlControls;
+using System.Diagnostics;
namespace DotNetOpenId.Yadis {
class Yadis {
internal const string HeaderName = "X-XRDS-Location";
- public static DiscoveryResult Discover(UriIdentifier uri) {
- var response = UntrustedWebRequest.Request(uri, null,
- new[] { ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds });
- if (response.StatusCode != System.Net.HttpStatusCode.OK) {
+ /// <summary>
+ /// Performs YADIS discovery on some identifier.
+ /// </summary>
+ /// <param name="uri">The URI to perform discovery on.</param>
+ /// <param name="requireSsl">Whether discovery should fail if any step of it is not encrypted.</param>
+ /// <returns>
+ /// The result of discovery on the given URL.
+ /// Null may be returned if an error occurs,
+ /// or if <paramref name="requireSsl"/> is true but part of discovery
+ /// is not protected by SSL.
+ /// </returns>
+ public static DiscoveryResult Discover(UriIdentifier uri, bool requireSsl) {
+ UntrustedWebResponse response;
+ try {
+ if (requireSsl && !string.Equals(uri.Uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ Logger.WarnFormat("Discovery on insecure identifier '{0}' aborted.", uri);
+ return null;
+ }
+ response = UntrustedWebRequest.Request(uri, null,
+ new[] { ContentTypes.Html, ContentTypes.XHtml, ContentTypes.Xrds }, requireSsl);
+ if (response.StatusCode != System.Net.HttpStatusCode.OK) {
+ Logger.ErrorFormat("HTTP error {0} {1} while performing discovery on {2}.", (int)response.StatusCode, response.StatusCode, uri);
+ return null;
+ }
+ } catch (ArgumentException ex) {
+ // Unsafe URLs generate this
+ Logger.WarnFormat("Unsafe OpenId URL detected ({0}). Request aborted. {1}", uri, ex);
return null;
}
UntrustedWebResponse response2 = null;
- if (response.ContentType.MediaType == ContentTypes.Xrds) {
+ if (isXrdsDocument(response)) {
+ Logger.Debug("An XRDS response was received from GET at user-supplied identifier.");
response2 = response;
} else {
string uriString = response.Headers.Get(HeaderName);
Uri url = null;
- if (uriString != null)
- Uri.TryCreate(uriString, UriKind.Absolute, out url);
- if (url == null && response.ContentType.MediaType == ContentTypes.Html)
+ if (uriString != null) {
+ if (Uri.TryCreate(uriString, UriKind.Absolute, out url)) {
+ Logger.DebugFormat("{0} found in HTTP header. Preparing to pull XRDS from {1}", HeaderName, url);
+ }
+ }
+ if (url == null && (response.ContentType.MediaType == ContentTypes.Html || response.ContentType.MediaType == ContentTypes.XHtml)) {
url = FindYadisDocumentLocationInHtmlMetaTags(response.ReadResponseString());
+ if (url != null) {
+ Logger.DebugFormat("{0} found in HTML Http-Equiv tag. Preparing to pull XRDS from {1}", HeaderName, url);
+ }
+ }
if (url != null) {
- response2 = UntrustedWebRequest.Request(url);
- if (response2.StatusCode != System.Net.HttpStatusCode.OK) {
- return null;
+ if (!requireSsl || string.Equals(url.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) {
+ response2 = UntrustedWebRequest.Request(url, null, new[] { ContentTypes.Xrds }, requireSsl);
+ if (response2.StatusCode != System.Net.HttpStatusCode.OK) {
+ return null;
+ }
+ } else {
+ Logger.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url);
}
}
}
return new DiscoveryResult(uri, response, response2);
}
+ private static bool isXrdsDocument(UntrustedWebResponse response) {
+ if (response.ContentType.MediaType == ContentTypes.Xrds) {
+ return true;
+ }
+
+ if (response.ContentType.MediaType == ContentTypes.Xml) {
+ // This COULD be an XRDS document with an imprecise content-type.
+ XmlReader reader = XmlReader.Create(new StringReader(response.ReadResponseString()));
+ while (reader.Read() && reader.NodeType != XmlNodeType.Element) ;
+ if (reader.NamespaceURI == XrdsNode.XrdsNamespace && reader.Name == "XRDS") {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/// <summary>
/// Searches an HTML document for a
/// &lt;meta http-equiv="X-XRDS-Location" content="{YadisURL}"&gt;
@@ -64,12 +117,14 @@ namespace DotNetOpenId.Yadis {
if (finalResponse == null) {
ContentType = initialResponse.ContentType;
ResponseText = initialResponse.ReadResponseString();
+ IsXrds = ContentType.MediaType == ContentTypes.Xrds;
} else {
ContentType = finalResponse.ContentType;
ResponseText = finalResponse.ReadResponseString();
- }
- if ((initialResponse != finalResponse) && (finalResponse != null)) {
- YadisLocation = finalResponse.RequestUri;
+ IsXrds = true;
+ if (initialResponse != finalResponse) {
+ YadisLocation = finalResponse.RequestUri;
+ }
}
}
@@ -103,9 +158,7 @@ namespace DotNetOpenId.Yadis {
/// Whether the <see cref="ResponseText"/> represents an XRDS document.
/// False if the response is an HTML document.
/// </summary>
- public bool IsXrds {
- get { return UsedYadisLocation || ContentType.MediaType == ContentTypes.Xrds; }
- }
+ public bool IsXrds { get; private set; }
/// <summary>
/// True if the response to the userSuppliedIdentifier pointed to a different URL
/// for the XRDS document.