diff options
Diffstat (limited to 'src/DotNetOpenId')
63 files changed, 1970 insertions, 379 deletions
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..492f757 100644 --- a/src/DotNetOpenId/CryptUtil.cs +++ b/src/DotNetOpenId/DiffieHellmanUtil.cs @@ -1,13 +1,48 @@ -using System;
+using System;
+using System.Globalization;
using System.Collections.Generic;
-using System.Text;
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 List<DHSha> {
+ 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),
+ }.ToArray();
+
+ 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 +54,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 +74,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 6916d24..9360b99 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>
@@ -21,7 +21,7 @@ <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>
@@ -49,6 +49,7 @@ <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" />
@@ -59,7 +60,23 @@ <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\ExtensionManager.cs" />
+ <Compile Include="Provider\ProviderSecuritySettings.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" />
@@ -78,7 +95,6 @@ <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" />
@@ -96,6 +112,7 @@ <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" />
@@ -111,7 +128,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" />
@@ -136,7 +153,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" />
@@ -193,4 +209,4 @@ </ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="..\..\tools\DotNetOpenId.Versioning.targets" />
-</Project>
+</Project> 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/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..9822bf1 --- /dev/null +++ b/src/DotNetOpenId/HmacShaAssociation.cs @@ -0,0 +1,142 @@ +using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+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 List<HmacSha> {
+ 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(),
+ },
+ }.ToArray();
+
+ 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);
+ 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);
+ }
+ }
+}
diff --git a/src/DotNetOpenId/IEncodable.cs b/src/DotNetOpenId/IEncodable.cs index 4dd830b..18a0ba2 100644 --- a/src/DotNetOpenId/IEncodable.cs +++ b/src/DotNetOpenId/IEncodable.cs @@ -33,6 +33,5 @@ namespace DotNetOpenId { /// Does not apply to <see cref="DotNetOpenId.EncodingType.DirectResponse"/>.
/// </summary>
Uri RedirectUrl { get; }
- Protocol Protocol { get; }
}
}
diff --git a/src/DotNetOpenId/Identifier.cs b/src/DotNetOpenId/Identifier.cs index 54e9c36..aa733a3 100644 --- a/src/DotNetOpenId/Identifier.cs +++ b/src/DotNetOpenId/Identifier.cs @@ -11,6 +11,26 @@ namespace DotNetOpenId { /// </summary>
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")]
@@ -115,5 +135,21 @@ namespace DotNetOpenId { /// 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/NoDiscoveryIdentifier.cs b/src/DotNetOpenId/NoDiscoveryIdentifier.cs new file mode 100644 index 0000000..8f772c4 --- /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)
+ : base(false) {
+ 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());
+ }
+
+ 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/OpenIdException.cs b/src/DotNetOpenId/OpenIdException.cs index 3285494..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)
diff --git a/src/DotNetOpenId/Protocol.cs b/src/DotNetOpenId/Protocol.cs index a9b7cf2..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,20 +112,29 @@ 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.ClaimedIdentifierServiceTypeURI, serviceTypeURIs) ??
+ Util.FindBestVersion(p => p.RPReturnToTypeURI, serviceTypeURIs);
}
/// <summary>
@@ -129,6 +142,18 @@ namespace DotNetOpenId { /// </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;
@@ -265,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";
diff --git a/src/DotNetOpenId/Provider/AssociateRequest.cs b/src/DotNetOpenId/Provider/AssociateRequest.cs index 8e5f178..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>
@@ -79,7 +79,7 @@ namespace DotNetOpenId.Provider { 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 82f979f..bda4918 100644 --- a/src/DotNetOpenId/Provider/CheckAuthRequest.cs +++ b/src/DotNetOpenId/Provider/CheckAuthRequest.cs @@ -70,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 314e813..ec6087b 100644 --- a/src/DotNetOpenId/Provider/CheckIdRequest.cs +++ b/src/DotNetOpenId/Provider/CheckIdRequest.cs @@ -98,17 +98,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;
@@ -118,17 +117,16 @@ 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;
}
+
+ claimedIdentifier = value;
}
}
@@ -248,7 +246,7 @@ namespace DotNetOpenId.Provider { }
}
- 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);
}
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 38f4be0..4716642 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.
diff --git a/src/DotNetOpenId/Provider/IdentityEndpoint.cs b/src/DotNetOpenId/Provider/IdentityEndpoint.cs index 604619b..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")]
diff --git a/src/DotNetOpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenId/Provider/OpenIdProvider.cs index b493fda..0fdb4b4 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.GetQueryFromContextNVC(); } }
+
/// <summary>
/// Constructs an OpenId server that uses the HttpApplication dictionary as
/// its association store and detects common settings.
@@ -43,7 +47,7 @@ namespace DotNetOpenId.Provider { /// This method requires a current ASP.NET HttpContext.
/// </remarks>
public OpenIdProvider()
- : this(HttpApplicationStore,
+ : this(Configuration.Store.CreateInstanceOfStore(HttpApplicationStore),
getProviderEndpointFromContext(), Util.GetRequestUrlFromContext(), Util.GetQueryFromContext()) { }
/// <summary>
/// Constructs an OpenId server that uses a given query and IAssociationStore.
@@ -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>
@@ -189,5 +201,16 @@ namespace DotNetOpenId.Provider { 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..708da31 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>
@@ -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 7813a2b..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.
diff --git a/src/DotNetOpenId/Provider/Signatory.cs b/src/DotNetOpenId/Provider/Signatory.cs index 53c6139..eaf3d21 100644 --- a/src/DotNetOpenId/Provider/Signatory.cs +++ b/src/DotNetOpenId/Provider/Signatory.cs @@ -79,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);
@@ -108,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);
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 c48265e..f7bb361 100644 --- a/src/DotNetOpenId/Realm.cs +++ b/src/DotNetOpenId/Realm.cs @@ -268,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.
diff --git a/src/DotNetOpenId/RelyingParty/AssociateRequest.cs b/src/DotNetOpenId/RelyingParty/AssociateRequest.cs index 89b8182..b2d4751 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,35 +10,44 @@ 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;
+ if (HmacShaAssociation.TryFindBestAssociation(provider.Protocol,
+ relyingParty.Settings.MinimumHashBitLength, relyingParty.Settings.MaximumHashBitLength,
+ true, 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);
- Logger.InfoFormat("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;
@@ -48,27 +57,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
@@ -76,10 +90,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 fddd397..bccdca5 100644 --- a/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs +++ b/src/DotNetOpenId/RelyingParty/AuthenticationRequest.cs @@ -29,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;
@@ -51,12 +52,17 @@ namespace DotNetOpenId.RelyingParty { AddCallbackArguments(DotNetOpenId.RelyingParty.Token.TokenKey, token);
}
internal static AuthenticationRequest Create(Identifier userSuppliedIdentifier,
- OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl, IRelyingPartyApplicationStore store) {
+ OpenIdRelyingParty relyingParty, Realm realm, Uri returnToUrl) {
if (userSuppliedIdentifier == null) throw new ArgumentNullException("userSuppliedIdentifier");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
if (realm == null) throw new ArgumentNullException("realm");
userSuppliedIdentifier = userSuppliedIdentifier.TrimFragment();
+ if (relyingParty.Settings.RequireSsl) {
+ // Rather than check for successful SSL conversion at this stage,
+ // We'll wait for secure discovery to fail on the new identifier.
+ userSuppliedIdentifier.TryRequireSsl(out userSuppliedIdentifier);
+ }
Logger.InfoFormat("Creating authentication request for user supplied Identifier: {0}",
userSuppliedIdentifier);
Logger.DebugFormat("Realm: {0}", realm);
@@ -73,10 +79,9 @@ namespace DotNetOpenId.RelyingParty { }
var endpoints = new List<ServiceEndpoint>(userSuppliedIdentifier.Discover());
- ServiceEndpoint endpoint = selectEndpoint(endpoints.AsReadOnly(), relyingParty, store);
+ ServiceEndpoint endpoint = selectEndpoint(endpoints.AsReadOnly(), relyingParty);
if (endpoint == null)
throw new OpenIdException(Strings.OpenIdEndpointNotFound);
- Logger.DebugFormat("Discovered provider endpoint: {0}", endpoint);
// 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
@@ -85,13 +90,13 @@ namespace DotNetOpenId.RelyingParty { throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.ReturnToNotUnderRealm, returnToUrl, realm));
- string token = new Token(endpoint).Serialize(store);
+ 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 = store != null ? getAssociation(endpoint, store, false) : null;
+ Association association = relyingParty.Store != null ? getAssociation(relyingParty, endpoint, false) : null;
return new AuthenticationRequest(
- token, association, endpoint, realm, returnToUrl, relyingParty.Encoder);
+ token, association, endpoint, realm, returnToUrl, relyingParty);
}
/// <summary>
@@ -102,11 +107,13 @@ namespace DotNetOpenId.RelyingParty { if (endpoints == null) throw new ArgumentNullException("endpoints");
if (relyingParty == null) throw new ArgumentNullException("relyingParty");
- // Filter the endpoints based on criteria given by the host web site.
- List<IXrdsProviderEndpoint> filteredEndpoints = new List<IXrdsProviderEndpoint>(endpoints.Count);
- var filter = relyingParty.EndpointFilter;
+ // 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 (filter == null || filter(endpoint)) {
+ if (versionFilter(endpoint) && hostingSiteFilter(endpoint)) {
filteredEndpoints.Add(endpoint);
}
}
@@ -125,10 +132,21 @@ namespace DotNetOpenId.RelyingParty { /// 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, IRelyingPartyApplicationStore store) {
+ 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) {
@@ -137,7 +155,8 @@ namespace DotNetOpenId.RelyingParty { // 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 (store == null) {
+ if (relyingParty.Store == null) {
+ Logger.Debug("No state store, so the first endpoint available is selected.");
return filteredEndpoints[0];
}
@@ -146,11 +165,14 @@ namespace DotNetOpenId.RelyingParty { // 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(endpointCandidate, store, true);
+ 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;
}
@@ -158,15 +180,29 @@ namespace DotNetOpenId.RelyingParty { // 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(ServiceEndpoint provider, IRelyingPartyApplicationStore store, bool createNewAssociationIfNeeded) {
+ 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) && createNewAssociationIfNeeded) {
- var req = AssociateRequest.Create(provider);
+ 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) {
@@ -174,9 +210,29 @@ namespace DotNetOpenId.RelyingParty { req = req.Response.SecondAttempt;
}
assoc = req.Response.Association;
+ // Confirm that the association matches the type we requested (section 8.2.1)
+ // if this is a 2.0 OP (1.x OPs had freedom to differ from the requested type).
+ if (assoc != null && provider.Protocol.Version.Major >= 2) {
+ if (!string.Equals(
+ req.Args[provider.Protocol.openid.assoc_type],
+ Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.assoc_type),
+ StringComparison.Ordinal) ||
+ !string.Equals(
+ req.Args[provider.Protocol.openid.session_type],
+ Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.session_type),
+ StringComparison.Ordinal)) {
+ Logger.ErrorFormat("Provider responded with contradicting association parameters. Requested [{0}, {1}] but got [{2}, {3}] back.",
+ req.Args[provider.Protocol.openid.assoc_type],
+ req.Args[provider.Protocol.openid.session_type],
+ Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.assoc_type),
+ Util.GetRequiredArg(req.Response.Args, provider.Protocol.openidnp.session_type));
+
+ assoc = null;
+ }
+ }
if (assoc != null) {
Logger.InfoFormat("Association with {0} established.", provider.ProviderEndpoint);
- store.StoreAssociation(provider.ProviderEndpoint, assoc);
+ relyingParty.Store.StoreAssociation(provider.ProviderEndpoint, assoc);
} else {
Logger.ErrorFormat("Association attempt with {0} provider failed.", provider.ProviderEndpoint);
}
@@ -215,6 +271,7 @@ namespace DotNetOpenId.RelyingParty { /// location.
/// </summary>
IProviderEndpoint IAuthenticationRequest.Provider { get { return endpoint; } }
+
/// <summary>
/// Gets the response to send to the user agent to begin the
/// OpenID authentication process.
@@ -245,7 +302,7 @@ namespace DotNetOpenId.RelyingParty { qsArgs.Add(pair.Key, pair.Value);
var request = new IndirectMessageRequest(this.endpoint.ProviderEndpoint, qsArgs);
- return this.encoder.Encode(request);
+ return RelyingParty.Encoder.Encode(request);
}
}
diff --git a/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/AuthenticationResponse.cs index 87424fa..903366b 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,7 +36,7 @@ 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");
@@ -74,8 +77,12 @@ namespace DotNetOpenId.RelyingParty { /// An Identifier that the end user claims to own.
/// </summary>
public Identifier ClaimedIdentifier {
- [DebuggerStepThrough]
- 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.
@@ -146,7 +153,7 @@ namespace DotNetOpenId.RelyingParty { }
internal static AuthenticationResponse Parse(IDictionary<string, string> query,
- IRelyingPartyApplicationStore store, Uri requestUrl) {
+ OpenIdRelyingParty relyingParty, Uri requestUrl) {
if (query == null) throw new ArgumentNullException("query");
if (requestUrl == null) throw new ArgumentNullException("requestUrl");
@@ -160,7 +167,7 @@ namespace DotNetOpenId.RelyingParty { HttpUtility.ParseQueryString(requestUrl.Query));
string token = Util.GetOptionalArg(requestUrlQuery, Token.TokenKey);
if (token != null) {
- tokenEndpoint = Token.Deserialize(token, store).Endpoint;
+ tokenEndpoint = Token.Deserialize(token, relyingParty.Store).Endpoint;
}
Protocol protocol = Protocol.Detect(query);
@@ -196,7 +203,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);
} else {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue,
@@ -206,7 +213,7 @@ namespace DotNetOpenId.RelyingParty { static AuthenticationResponse parseIdResResponse(IDictionary<string, string> query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint,
- IRelyingPartyApplicationStore store, Uri requestUrl) {
+ OpenIdRelyingParty relyingParty, Uri requestUrl) {
// Use responseEndpoint if it is available so we get the
// Claimed Identifer correct in the AuthenticationResponse.
ServiceEndpoint unverifiedEndpoint = responseEndpoint ?? tokenEndpoint;
@@ -218,9 +225,9 @@ namespace DotNetOpenId.RelyingParty { }
verifyReturnTo(query, unverifiedEndpoint, requestUrl);
- verifyDiscoveredInfoMatchesAssertedInfo(query, tokenEndpoint, responseEndpoint);
- verifyNonceUnused(query, unverifiedEndpoint, store);
- verifySignature(query, unverifiedEndpoint, store);
+ verifyDiscoveredInfoMatchesAssertedInfo(relyingParty, query, tokenEndpoint, responseEndpoint);
+ verifyNonceUnused(query, unverifiedEndpoint, relyingParty.Store);
+ verifySignature(relyingParty, query, unverifiedEndpoint);
return new AuthenticationResponse(AuthenticationStatus.Authenticated, unverifiedEndpoint, query);
}
@@ -260,7 +267,8 @@ namespace DotNetOpenId.RelyingParty { /// <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...");
@@ -277,22 +285,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 == 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 == tokenEndpoint.Protocol.ClaimedIdentifierForOPIdentifier) { // or directed identity is in effect
Identifier claimedIdentifier = Util.GetRequiredArg(query, responseEndpoint.Protocol.openid.claimed_id);
+ // 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(Strings.IssuedAssertionFailsIdentifierDiscovery);
+ throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
+ Strings.IssuedAssertionFailsIdentifierDiscovery,
+ responseEndpoint, Util.ToString(discoveredEndpoints)));
}
- } else {
- // Check that the assertion matches the service endpoint we know about.
- if (responseEndpoint != tokenEndpoint)
- throw new OpenIdException(Strings.IssuedAssertionFailsIdentifierDiscovery);
}
}
}
@@ -306,7 +320,7 @@ namespace DotNetOpenId.RelyingParty { 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(',');
@@ -329,13 +343,13 @@ 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.
Logger.Debug("Passing signature back to Provider for verification (no association available)...");
- verifySignatureByProvider(query, endpoint, store);
+ verifySignatureByProvider(relyingParty, query, endpoint);
} else {
if (assoc.IsExpired)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
@@ -378,12 +392,33 @@ 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);
}
+
+ #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/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 dbc91f8..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,45 +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() {
Logger.DebugFormat("Sending direct message to {0}: {1}{2}", Provider.ProviderEndpoint,
Environment.NewLine, Util.ToString(Args));
- 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);
- Logger.DebugFormat("Received direct response from {0}: {1}{2}", Provider.ProviderEndpoint,
- Environment.NewLine, Util.ToString(args));
- } 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));
- }
+ return RelyingParty.DirectMessageChannel.SendDirectMessageAndGetResponse(Provider, Args);
}
}
}
diff --git a/src/DotNetOpenId/RelyingParty/DirectResponse.cs b/src/DotNetOpenId/RelyingParty/DirectResponse.cs index 8c3cec1..6a88a3a 100644 --- a/src/DotNetOpenId/RelyingParty/DirectResponse.cs +++ b/src/DotNetOpenId/RelyingParty/DirectResponse.cs @@ -2,16 +2,30 @@ 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;
+ // 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)) {
@@ -22,10 +36,10 @@ namespace DotNetOpenId.RelyingParty { }
}
}
-
}
+ 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/IAuthenticationResponse.cs b/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs index 26b6c03..8cf2dbe 100644 --- a/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs +++ b/src/DotNetOpenId/RelyingParty/IAuthenticationResponse.cs @@ -30,6 +30,7 @@ namespace DotNetOpenId.RelyingParty { IExtensionResponse GetExtension(Type extensionType);
/// <summary>
/// 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>
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/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 index bc25747..78c873f 100644 --- a/src/DotNetOpenId/RelyingParty/IXrdsProviderEndpoint.cs +++ b/src/DotNetOpenId/RelyingParty/IXrdsProviderEndpoint.cs @@ -8,6 +8,10 @@ namespace DotNetOpenId.RelyingParty { [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>
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/OpenIdLogin.cs b/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs index 48df69c..564f0e7 100644 --- a/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs +++ b/src/DotNetOpenId/RelyingParty/OpenIdLogin.cs @@ -20,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;
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs index 1e727a8..ef96ffd 100644 --- a/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs +++ b/src/DotNetOpenId/RelyingParty/OpenIdRelyingParty.cs @@ -2,8 +2,10 @@ using System; using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
+using System.Configuration;
using System.Diagnostics;
using System.Web;
+using DotNetOpenId.Configuration;
namespace DotNetOpenId.RelyingParty {
/// <summary>
@@ -82,12 +84,16 @@ namespace DotNetOpenId.RelyingParty { ///}
/// </code>
/// </example>
- [DebuggerDisplay("isAuthenticationResponseReady: {isAuthenticationResponseReady}, stateless: {store == null}")]
+ [DebuggerDisplay("isAuthenticationResponseReady: {isAuthenticationResponseReady}, stateless: {Store == null}")]
public class OpenIdRelyingParty {
- IRelyingPartyApplicationStore store;
+ internal IRelyingPartyApplicationStore Store;
Uri request;
IDictionary<string, string> query;
MessageEncoder encoder = new MessageEncoder();
+ internal IDirectMessageChannel DirectMessageChannel = new DirectMessageHttpChannel();
+
+ internal static Uri DefaultRequestUrl { get { return Util.GetRequestUrlFromContext(); } }
+ internal static NameValueCollection DefaultQuery { get { return Util.GetQueryFromContextNVC(); } }
/// <summary>
/// Constructs an OpenId consumer that uses the current HttpContext request
@@ -97,7 +103,7 @@ namespace DotNetOpenId.RelyingParty { /// This method requires a current ASP.NET HttpContext.
/// </remarks>
public OpenIdRelyingParty()
- : this(HttpApplicationStore,
+ : this(Configuration.Store.CreateInstanceOfStore(HttpApplicationStore),
Util.GetRequestUrlFromContext(), Util.GetQueryFromContext()) { }
/// <summary>
/// Constructs an OpenId consumer that uses a given querystring and IAssociationStore.
@@ -131,7 +137,11 @@ namespace DotNetOpenId.RelyingParty { 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.
}
@@ -163,7 +173,7 @@ 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, this, realm, returnToUrl, store);
+ return AuthenticationRequest.Create(userSuppliedIdentifier, this, realm, returnToUrl);
}
/// <summary>
@@ -274,7 +284,7 @@ namespace DotNetOpenId.RelyingParty { get {
if (response == null && isAuthenticationResponseReady) {
try {
- response = AuthenticationResponse.Parse(query, store, request);
+ response = AuthenticationResponse.Parse(query, this, request);
} catch (OpenIdException ex) {
response = new FailedAuthenticationResponse(ex);
}
@@ -315,10 +325,13 @@ namespace DotNetOpenId.RelyingParty { [EditorBrowsable(EditorBrowsableState.Advanced)]
public static Comparison<IXrdsProviderEndpoint> DefaultEndpointOrder {
get {
- // Sort first by Service/@priority, then by Service/Uri/@priority
+ // 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) {
- int result = se1.ServicePriority.Value.CompareTo(se2.ServicePriority.Value);
+ 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);
@@ -351,6 +364,24 @@ namespace DotNetOpenId.RelyingParty { }
}
+ 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>
@@ -386,6 +417,25 @@ 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>
diff --git a/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs b/src/DotNetOpenId/RelyingParty/OpenIdTextBox.cs index b4b55c8..26b1aa8 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.
@@ -179,6 +179,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.
@@ -444,6 +458,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
@@ -554,7 +592,7 @@ namespace DotNetOpenId.RelyingParty base.OnLoad(e);
if (!Enabled || Page.IsPostBack) return;
- var consumer = new OpenIdRelyingParty();
+ var consumer = createRelyingParty();
if (consumer.Response != null) {
string persistentString = Page.Request.QueryString[usePersistentCookieCallbackKey];
bool persistentBool;
@@ -581,6 +619,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>
@@ -621,7 +676,7 @@ namespace DotNetOpenId.RelyingParty throw new InvalidOperationException(DotNetOpenId.Strings.OpenIdTextBoxEmpty);
try {
- var consumer = new OpenIdRelyingParty();
+ var consumer = createRelyingParty();
// 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
@@ -791,6 +846,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>
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 f6946bf..b5ede55 100644 --- a/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs +++ b/src/DotNetOpenId/RelyingParty/ServiceEndpoint.cs @@ -24,6 +24,12 @@ 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>
@@ -63,16 +69,17 @@ namespace DotNetOpenId.RelyingParty { if (UserSuppliedIdentifier == null || String.Equals(UserSuppliedIdentifier, ClaimedIdentifier, StringComparison.OrdinalIgnoreCase)) {
friendlyIdentifierForDisplay = ClaimedIdentifier;
} else {
- friendlyIdentifierForDisplay = String.Format(CultureInfo.CurrentCulture, "{0} ({1})",
- ClaimedIdentifier, UserSuppliedIdentifier);
+ friendlyIdentifierForDisplay = UserSuppliedIdentifier;
}
} else if (uri != null) {
- 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);
+ 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;
@@ -83,7 +90,7 @@ namespace DotNetOpenId.RelyingParty { }
/// <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; }
@@ -156,6 +163,10 @@ namespace DotNetOpenId.RelyingParty { }
}
+ 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.");
@@ -265,7 +276,27 @@ 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
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 8382b9b..4798c7f 100644 --- a/src/DotNetOpenId/Strings.Designer.cs +++ b/src/DotNetOpenId/Strings.Designer.cs @@ -88,6 +88,15 @@ namespace DotNetOpenId { }
/// <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 must be set first..
/// </summary>
internal static string ClaimedIdentifierMustBeSetFirst {
@@ -142,6 +151,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 '{0}' has already been added. Only one extension per namespace is allowed in a given request..
/// </summary>
internal static string ExtensionAlreadyAddedWithSameTypeURI {
@@ -196,20 +214,20 @@ 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 '{0}' and session type '{1}', 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);
}
}
@@ -223,6 +241,15 @@ namespace DotNetOpenId { }
/// <summary>
+ /// Looks up a localized string similar to Insecure web request for '{0}' 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 '{0}' because it contains an illegal character for Key-Value Form encoding. (line {1}: '{2}').
/// </summary>
internal static string InvalidCharacterInKeyValueFormInput {
@@ -322,7 +349,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 {
@@ -358,6 +389,15 @@ namespace DotNetOpenId { }
/// <summary>
+ /// 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 {
@@ -421,6 +461,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 {
@@ -520,6 +569,15 @@ namespace DotNetOpenId { }
/// <summary>
+ /// Looks up a localized string similar to The maximum allowable number of redirects were exceeded while requesting '{0}'..
+ /// </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 {
@@ -545,5 +603,23 @@ namespace DotNetOpenId { return ResourceManager.GetString("UnspecifiedDateTimeKindNotAllowed", 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 53b3d4a..ab769a7 100644 --- a/src/DotNetOpenId/Strings.resx +++ b/src/DotNetOpenId/Strings.resx @@ -126,6 +126,9 @@ <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="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>
@@ -144,6 +147,9 @@ <data name="ExpiredNonce" xml:space="preserve">
<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>
</data>
@@ -162,15 +168,18 @@ <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. (line {1}: '{2}')</value>
</data>
@@ -205,7 +214,11 @@ <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>
@@ -216,6 +229,9 @@ <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 for XRI {0} is missing the required CanonicalID element.</value>
</data>
@@ -237,6 +253,9 @@ <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>
@@ -270,6 +289,9 @@ <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>
@@ -279,4 +301,10 @@ <data name="UnspecifiedDateTimeKindNotAllowed" xml:space="preserve">
<value>Providing a DateTime whose Kind is Unspecified is not allowed.</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>
</root>
\ No newline at end of file diff --git a/src/DotNetOpenId/UntrustedWebRequest.cs b/src/DotNetOpenId/UntrustedWebRequest.cs index 852f21f..f10fdd4 100644 --- a/src/DotNetOpenId/UntrustedWebRequest.cs +++ b/src/DotNetOpenId/UntrustedWebRequest.cs @@ -9,6 +9,8 @@ namespace DotNetOpenId { using System.IO;
using System.Net;
using System.Text.RegularExpressions;
+ using System.Configuration;
+ using DotNetOpenId.Configuration;
/// <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
@@ -24,8 +26,12 @@ namespace DotNetOpenId { /// If a particular host would not be permitted but is in the whitelist, it is allowed.
/// </remarks>
public static class UntrustedWebRequest {
+ 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.
@@ -38,7 +44,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.
@@ -73,8 +79,8 @@ namespace DotNetOpenId { [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
static UntrustedWebRequest() {
- ReadWriteTimeout = TimeSpan.FromMilliseconds(800);
- Timeout = TimeSpan.FromSeconds(10);
+ ReadWriteTimeout = Configuration.ReadWriteTimeout;
+ Timeout = Configuration.Timeout;
#if LONGTIMEOUT
ReadWriteTimeout = TimeSpan.FromHours(1);
Timeout = TimeSpan.FromHours(1);
@@ -90,25 +96,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.
@@ -223,11 +229,36 @@ 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) {
@@ -235,10 +266,13 @@ namespace DotNetOpenId { }
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.
+ request.AllowAutoRedirect = false;
request.ReadWriteTimeout = (int)ReadWriteTimeout.TotalMilliseconds;
request.Timeout = (int)Timeout.TotalMilliseconds;
request.KeepAlive = false;
- request.MaximumAutomaticRedirections = MaximumRedirections;
if (acceptTypes != null)
request.Accept = string.Join(",", acceptTypes);
if (body != null) {
@@ -266,17 +300,17 @@ namespace DotNetOpenId { }
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) {
- return getResponse(uri, response);
+ return getResponse(originalRequestUri, 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, response);
} else {
throw;
}
diff --git a/src/DotNetOpenId/UriIdentifier.cs b/src/DotNetOpenId/UriIdentifier.cs index 6371a59..a0a31c4 100644 --- a/src/DotNetOpenId/UriIdentifier.cs +++ b/src/DotNetOpenId/UriIdentifier.cs @@ -18,21 +18,40 @@ namespace DotNetOpenId { return new UriIdentifier(identifier);
}
- public UriIdentifier(string uri) {
+ public UriIdentifier(string uri) : this(uri, false) { }
+ public 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) {
+ public UriIdentifier(Uri uri) : this(uri, false) { }
+ public 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; }
+ /// <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;
@@ -41,15 +60,20 @@ 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) {
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 (!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) {
@@ -82,7 +106,8 @@ namespace DotNetOpenId { }
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;
@@ -150,18 +175,33 @@ namespace DotNetOpenId { internal override IEnumerable<ServiceEndpoint> Discover() {
List<ServiceEndpoint> endpoints = new List<ServiceEndpoint>();
// Attempt YADIS discovery
- DiscoveryResult yadisResult = Yadis.Yadis.Discover(this);
+ DiscoveryResult yadisResult = Yadis.Yadis.Discover(this, IsDiscoverySecureEndToEnd);
if (yadisResult != null) {
if (yadisResult.IsXrds) {
XrdsDocument xrds = new XrdsDocument(yadisResult.ResponseText);
- endpoints.AddRange(xrds.CreateServiceEndpoints(yadisResult.NormalizedUri));
+ 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);
}
// Failing YADIS discovery of an XRDS document, we try HTML discovery.
if (endpoints.Count == 0) {
ServiceEndpoint ep = DiscoverFromHtml(yadisResult.NormalizedUri, yadisResult.ResponseText);
if (ep != null) {
- endpoints.Add(ep);
+ Logger.Debug("HTML discovery found a service endpoint.");
+ Logger.Debug(ep);
+ if (!IsDiscoverySecureEndToEnd || ep.IsSecure) {
+ endpoints.Add(ep);
+ } else {
+ Logger.Info("Skipping HTML discovered endpoint because it is not secure.");
+ }
+ } else {
+ Logger.Debug("HTML discovery failed to find any endpoints.");
}
+ } else {
+ Logger.Debug("Skipping HTML discovery because XRDS contained service endpoints.");
}
}
return endpoints;
@@ -178,6 +218,36 @@ namespace DotNetOpenId { return builder.Uri;
}
+ 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;
+ }
+ secureIdentifier = new UriIdentifier(newIdentifierUri.Uri, true);
+ return true;
+ }
+
+ // This identifier is explicitly NOT https, so we cannot change it.
+ secureIdentifier = new NoDiscoveryIdentifier(this);
+ return false;
+ }
+
public override bool Equals(object obj) {
UriIdentifier other = obj as UriIdentifier;
if (other == null) return false;
diff --git a/src/DotNetOpenId/Util.cs b/src/DotNetOpenId/Util.cs index 1eb7c5f..6568282 100644 --- a/src/DotNetOpenId/Util.cs +++ b/src/DotNetOpenId/Util.cs @@ -126,11 +126,14 @@ namespace DotNetOpenId { return nvc;
}
- public static IDictionary<string, string> GetQueryFromContext() {
+ 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);
+ return query;
+ }
+ public static IDictionary<string, string> GetQueryFromContext() {
+ return NameValueCollectionToDictionary(GetQueryFromContextNVC());
}
/// <summary>
/// Gets the original request URL, as seen from the browser before any URL rewrites on the server if any.
@@ -269,6 +272,42 @@ 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>
@@ -287,6 +326,49 @@ namespace DotNetOpenId { 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) {
diff --git a/src/DotNetOpenId/XriIdentifier.cs b/src/DotNetOpenId/XriIdentifier.cs index b412e79..51072d4 100644 --- a/src/DotNetOpenId/XriIdentifier.cs +++ b/src/DotNetOpenId/XriIdentifier.cs @@ -12,10 +12,18 @@ namespace DotNetOpenId { internal static readonly char[] GlobalContextSymbols = { '=', '@', '+', '$', '!' };
const string xriScheme = "xri://";
- public XriIdentifier(string xri) {
+ public XriIdentifier(string xri) : this(xri, false) { }
+ public 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);
}
@@ -57,25 +65,32 @@ namespace DotNetOpenId { /// 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 xriResolverProxy = "https://xri.net/{0}?_xrd_r=application/xrd%2Bxml;sep=false";
+ 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 {
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, this);
+ return downloadXrds().CreateServiceEndpoints(this);
}
/// <summary>
@@ -83,13 +98,18 @@ namespace DotNetOpenId { /// instances that treat another given identifier as the user-supplied identifier.
/// </summary>
internal IEnumerable<ServiceEndpoint> Discover(XriIdentifier userSuppliedIdentifier) {
- return downloadXrds().CreateServiceEndpoints(this, userSuppliedIdentifier);
+ return downloadXrds().CreateServiceEndpoints(userSuppliedIdentifier);
}
internal override Identifier TrimFragment() {
return this;
}
+ internal override bool TryRequireSsl(out Identifier secureIdentifier) {
+ secureIdentifier = IsDiscoverySecureEndToEnd ? this : new XriIdentifier(this, true);
+ return true;
+ }
+
public override bool Equals(object obj) {
XriIdentifier other = obj as XriIdentifier;
if (other == null) return false;
diff --git a/src/DotNetOpenId/Yadis/XrdElement.cs b/src/DotNetOpenId/Yadis/XrdElement.cs index d690298..5665f58 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 d3510e5..622165a 100644 --- a/src/DotNetOpenId/Yadis/XrdsDocument.cs +++ b/src/DotNetOpenId/Yadis/XrdsDocument.cs @@ -45,17 +45,21 @@ namespace DotNetOpenId.Yadis { 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 IEnumerable<ServiceEndpoint> CreateServiceEndpoints(XriIdentifier discoveredIdentifier, XriIdentifier 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(discoveredIdentifier, userSuppliedIdentifier));
+ endpoints.AddRange(generateClaimedIdentifierServiceEndpoints(userSuppliedIdentifier));
}
+ Logger.DebugFormat("Total services discovered in XRDS: {0}", endpoints.Count);
+ Logger.Debug(Util.ToString(endpoints, true));
return endpoints;
}
@@ -80,7 +84,7 @@ namespace DotNetOpenId.Yadis { }
}
- IEnumerable<ServiceEndpoint> generateClaimedIdentifierServiceEndpoints(XriIdentifier claimedIdentifier, XriIdentifier userSuppliedIdentifier) {
+ 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
@@ -88,24 +92,11 @@ namespace DotNetOpenId.Yadis { Logger.WarnFormat(Strings.MissingCanonicalIDElement, userSuppliedIdentifier);
break; // skip on to next service
}
- // In the case of XRI names, the ClaimedId is actually the CanonicalID.
- // Per http://dev.inames.net/wiki/XRI_CanonicalID_Verification as of 6/20/08,
- // we need to perform CanonicalId verification when using xri.net as our proxy resolver
- // to protect ourselves against a security vulnerability.
- // We do this by asking the proxy to resolve again, based on the CanonicalId that we
- // just got from the XRI i-name. We SHOULD get the same document back, but in case
- // of the attack it would be a different document, and the second document would be
- // the reliable one.
- if (performCIDVerification && claimedIdentifier != service.Xrd.CanonicalID) {
- Logger.InfoFormat("Performing XRI CanonicalID verification on user supplied identifier {0}, canonical id {1}.", userSuppliedIdentifier, service.Xrd.CanonicalID);
- XriIdentifier canonicalId = new XriIdentifier(service.Xrd.CanonicalID);
- foreach (var endpoint in canonicalId.Discover(userSuppliedIdentifier)) {
- yield return endpoint;
- }
- yield break;
- } else {
- claimedIdentifier = new XriIdentifier(service.Xrd.CanonicalID);
+ if (!service.Xrd.IsCanonicalIdVerified) {
+ throw new OpenIdException(Strings.CIDVerificationFailed, userSuppliedIdentifier);
}
+ // 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);
@@ -113,8 +104,6 @@ namespace DotNetOpenId.Yadis { }
}
- const bool performCIDVerification = true;
-
internal IEnumerable<RelyingPartyReceivingEndpoint> FindRelyingPartyReceivingEndpoints() {
foreach (var service in findReturnToServices()) {
foreach (var uri in service.UriElements) {
@@ -150,5 +139,16 @@ namespace DotNetOpenId.Yadis { }
}
}
+
+ internal bool IsXrdResolutionSuccessful {
+ get {
+ foreach (var xrd in XrdElements) {
+ if (!xrd.IsXriResolutionSuccessful) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
}
}
diff --git a/src/DotNetOpenId/Yadis/Yadis.cs b/src/DotNetOpenId/Yadis/Yadis.cs index ac0d3da..ec67f85 100644 --- a/src/DotNetOpenId/Yadis/Yadis.cs +++ b/src/DotNetOpenId/Yadis/Yadis.cs @@ -13,11 +13,26 @@ namespace DotNetOpenId.Yadis { class Yadis {
internal const string HeaderName = "X-XRDS-Location";
- public static DiscoveryResult Discover(UriIdentifier uri) {
+ /// <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 });
+ 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;
@@ -29,18 +44,30 @@ namespace DotNetOpenId.Yadis { }
UntrustedWebResponse response2 = null;
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) {
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, null, requireSsl);
+ if (response2.StatusCode != System.Net.HttpStatusCode.OK) {
+ return null;
+ }
+ } else {
+ Logger.WarnFormat("XRDS document at insecure location '{0}'. Aborting YADIS discovery.", url);
}
}
}
|