diff options
Diffstat (limited to 'src/DotNetOpenAuth')
46 files changed, 1210 insertions, 224 deletions
diff --git a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs index f535c38..7bd84d9 100644 --- a/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs +++ b/src/DotNetOpenAuth/Configuration/DotNetOpenAuthSection.cs @@ -30,6 +30,11 @@ namespace DotNetOpenAuth.Configuration { private const string OpenIdElementName = "openid"; /// <summary> + /// The name of the <oauth> sub-element. + /// </summary> + private const string OAuthElementName = "oauth"; + + /// <summary> /// Initializes a new instance of the <see cref="DotNetOpenAuthSection"/> class. /// </summary> internal DotNetOpenAuthSection() { @@ -61,5 +66,14 @@ namespace DotNetOpenAuth.Configuration { get { return (OpenIdElement)this[OpenIdElementName] ?? new OpenIdElement(); } set { this[OpenIdElementName] = value; } } + + /// <summary> + /// Gets or sets the configuration for OAuth. + /// </summary> + [ConfigurationProperty(OAuthElementName)] + internal OAuthElement OAuth { + get { return (OAuthElement)this[OAuthElementName] ?? new OAuthElement(); } + set { this[OAuthElementName] = value; } + } } } diff --git a/src/DotNetOpenAuth/Configuration/OAuthConsumerElement.cs b/src/DotNetOpenAuth/Configuration/OAuthConsumerElement.cs new file mode 100644 index 0000000..b15c3e3 --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/OAuthConsumerElement.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthConsumerElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + + /// <summary> + /// Represents the <oauth/consumer> element in the host's .config file. + /// </summary> + internal class OAuthConsumerElement : ConfigurationElement { + /// <summary> + /// Gets the name of the security sub-element. + /// </summary> + private const string SecuritySettingsConfigName = "security"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthConsumerElement"/> class. + /// </summary> + internal OAuthConsumerElement() { + } + + /// <summary> + /// Gets or sets the security settings. + /// </summary> + [ConfigurationProperty(SecuritySettingsConfigName)] + public OAuthConsumerSecuritySettingsElement SecuritySettings { + get { return (OAuthConsumerSecuritySettingsElement)this[SecuritySettingsConfigName] ?? new OAuthConsumerSecuritySettingsElement(); } + set { this[SecuritySettingsConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth/Configuration/OAuthConsumerSecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OAuthConsumerSecuritySettingsElement.cs new file mode 100644 index 0000000..5e75390 --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/OAuthConsumerSecuritySettingsElement.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthConsumerSecuritySettingsElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth; + + /// <summary> + /// Security settings that are applicable to consumers. + /// </summary> + internal class OAuthConsumerSecuritySettingsElement : ConfigurationElement { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthConsumerSecuritySettingsElement"/> class. + /// </summary> + internal OAuthConsumerSecuritySettingsElement() { + } + + /// <summary> + /// Initializes a programmatically manipulatable bag of these security settings with the settings from the config file. + /// </summary> + /// <returns>The newly created security settings object.</returns> + internal ConsumerSecuritySettings CreateSecuritySettings() { + return new ConsumerSecuritySettings(); + } + } +} diff --git a/src/DotNetOpenAuth/Configuration/OAuthElement.cs b/src/DotNetOpenAuth/Configuration/OAuthElement.cs new file mode 100644 index 0000000..282bdba --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/OAuthElement.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + + /// <summary> + /// Represents the <oauth> element in the host's .config file. + /// </summary> + internal class OAuthElement : ConfigurationElement { + /// <summary> + /// The name of the <consumer> sub-element. + /// </summary> + private const string ConsumerElementName = "consumer"; + + /// <summary> + /// The name of the <serviceProvider> sub-element. + /// </summary> + private const string ServiceProviderElementName = "serviceProvider"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthElement"/> class. + /// </summary> + internal OAuthElement() { + } + + /// <summary> + /// Gets or sets the configuration specific for Consumers. + /// </summary> + [ConfigurationProperty(ConsumerElementName)] + internal OAuthConsumerElement Consumer { + get { return (OAuthConsumerElement)this[ConsumerElementName] ?? new OAuthConsumerElement(); } + set { this[ConsumerElementName] = value; } + } + + /// <summary> + /// Gets or sets the configuration specific for Service Providers. + /// </summary> + [ConfigurationProperty(ServiceProviderElementName)] + internal OAuthServiceProviderElement ServiceProvider { + get { return (OAuthServiceProviderElement)this[ServiceProviderElementName] ?? new OAuthServiceProviderElement(); } + set { this[ServiceProviderElementName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth/Configuration/OAuthServiceProviderElement.cs b/src/DotNetOpenAuth/Configuration/OAuthServiceProviderElement.cs new file mode 100644 index 0000000..5ff528d --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/OAuthServiceProviderElement.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthServiceProviderElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + + /// <summary> + /// Represents the <oauth/serviceProvider> element in the host's .config file. + /// </summary> + internal class OAuthServiceProviderElement : ConfigurationElement { + /// <summary> + /// Gets the name of the security sub-element. + /// </summary> + private const string SecuritySettingsConfigName = "security"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthServiceProviderElement"/> class. + /// </summary> + internal OAuthServiceProviderElement() { + } + + /// <summary> + /// Gets or sets the security settings. + /// </summary> + [ConfigurationProperty(SecuritySettingsConfigName)] + public OAuthServiceProviderSecuritySettingsElement SecuritySettings { + get { return (OAuthServiceProviderSecuritySettingsElement)this[SecuritySettingsConfigName] ?? new OAuthServiceProviderSecuritySettingsElement(); } + set { this[SecuritySettingsConfigName] = value; } + } + } +} diff --git a/src/DotNetOpenAuth/Configuration/OAuthServiceProviderSecuritySettingsElement.cs b/src/DotNetOpenAuth/Configuration/OAuthServiceProviderSecuritySettingsElement.cs new file mode 100644 index 0000000..92d9645 --- /dev/null +++ b/src/DotNetOpenAuth/Configuration/OAuthServiceProviderSecuritySettingsElement.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthServiceProviderSecuritySettingsElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth; + + /// <summary> + /// Security settings that are applicable to service providers. + /// </summary> + internal class OAuthServiceProviderSecuritySettingsElement : ConfigurationElement { + /// <summary> + /// Gets the name of the @minimumRequiredOAuthVersion attribute. + /// </summary> + private const string MinimumRequiredOAuthVersionConfigName = "minimumRequiredOAuthVersion"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthServiceProviderSecuritySettingsElement"/> class. + /// </summary> + internal OAuthServiceProviderSecuritySettingsElement() { + } + + /// <summary> + /// Gets or sets the minimum OAuth version a Consumer is required to support in order for this library to interoperate with it. + /// </summary> + /// <remarks> + /// Although the earliest versions of OAuth are supported, for security reasons it may be desirable to require the + /// remote party to support a later version of OAuth. + /// </remarks> + [ConfigurationProperty(MinimumRequiredOAuthVersionConfigName, DefaultValue = "V10")] + public ProtocolVersion MinimumRequiredOAuthVersion { + get { return (ProtocolVersion)this[MinimumRequiredOAuthVersionConfigName]; } + set { this[MinimumRequiredOAuthVersionConfigName] = value; } + } + + /// <summary> + /// Initializes a programmatically manipulatable bag of these security settings with the settings from the config file. + /// </summary> + /// <returns>The newly created security settings object.</returns> + internal ServiceProviderSecuritySettings CreateSecuritySettings() { + return new ServiceProviderSecuritySettings { + MinimumRequiredOAuthVersion = this.MinimumRequiredOAuthVersion, + }; + } + } +} diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 441c714..34c39a3 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -177,6 +177,11 @@ <Compile Include="Configuration\AssociationTypeElement.cs" /> <Compile Include="Configuration\DotNetOpenAuthSection.cs" /> <Compile Include="Configuration\MessagingElement.cs" /> + <Compile Include="Configuration\OAuthConsumerElement.cs" /> + <Compile Include="Configuration\OAuthConsumerSecuritySettingsElement.cs" /> + <Compile Include="Configuration\OAuthElement.cs" /> + <Compile Include="Configuration\OAuthServiceProviderElement.cs" /> + <Compile Include="Configuration\OAuthServiceProviderSecuritySettingsElement.cs" /> <Compile Include="Configuration\OpenIdElement.cs" /> <Compile Include="Configuration\OpenIdProviderElement.cs" /> <Compile Include="Configuration\OpenIdProviderSecuritySettingsElement.cs" /> @@ -222,8 +227,9 @@ <Compile Include="Messaging\NetworkDirectWebResponse.cs" /> <Compile Include="Messaging\OutgoingWebResponseActionResult.cs" /> <Compile Include="Messaging\Reflection\IMessagePartEncoder.cs" /> + <Compile Include="Messaging\Reflection\IMessagePartNullEncoder.cs" /> <Compile Include="Messaging\Reflection\MessageDescriptionCollection.cs" /> - <Compile Include="OAuth\ChannelElements\IConsumerCertificateProvider.cs" /> + <Compile Include="OAuth\ChannelElements\IConsumerDescription.cs" /> <Compile Include="OAuth\ChannelElements\IConsumerTokenManager.cs" /> <Compile Include="OAuth\ChannelElements\IServiceProviderTokenManager.cs" /> <Compile Include="OAuth\ChannelElements\OAuthConsumerMessageFactory.cs" /> @@ -232,11 +238,15 @@ <Compile Include="OAuth\ChannelElements\OAuthHttpMethodBindingElement.cs" /> <Compile Include="OAuth\ChannelElements\PlaintextSigningBindingElement.cs" /> <Compile Include="OAuth\ChannelElements\HmacSha1SigningBindingElement.cs" /> + <Compile Include="OAuth\ChannelElements\IServiceProviderRequestToken.cs" /> <Compile Include="OAuth\ChannelElements\SigningBindingElementBaseContract.cs" /> <Compile Include="OAuth\ChannelElements\SigningBindingElementChain.cs" /> <Compile Include="OAuth\ChannelElements\StandardTokenGenerator.cs" /> <Compile Include="OAuth\ChannelElements\TokenType.cs" /> + <Compile Include="OAuth\ChannelElements\UriOrOobEncoding.cs" /> + <Compile Include="OAuth\ChannelElements\TokenHandlingBindingElement.cs" /> <Compile Include="OAuth\ConsumerBase.cs" /> + <Compile Include="OAuth\ConsumerSecuritySettings.cs" /> <Compile Include="OAuth\DesktopConsumer.cs" /> <Compile Include="GlobalSuppressions.cs" /> <Compile Include="OAuth\Messages\ITokenSecretContainingMessage.cs" /> @@ -247,11 +257,14 @@ <DesignTime>True</DesignTime> <DependentUpon>OAuthStrings.resx</DependentUpon> </Compile> + <Compile Include="OAuth\SecuritySettings.cs" /> <Compile Include="OAuth\ServiceProviderDescription.cs" /> <Compile Include="OAuth\Messages\ITokenContainingMessage.cs" /> <Compile Include="OAuth\Messages\SignedMessageBase.cs" /> <Compile Include="Messaging\Bindings\NonceMemoryStore.cs" /> <Compile Include="OAuth\ChannelElements\SigningBindingElementBase.cs" /> + <Compile Include="OAuth\ServiceProviderSecuritySettings.cs" /> + <Compile Include="OAuth\VerificationCodeFormat.cs" /> <Compile Include="OAuth\WebConsumer.cs" /> <Compile Include="Messaging\IDirectWebRequestHandler.cs" /> <Compile Include="OAuth\ChannelElements\ITamperResistantOAuthMessage.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs b/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs index bb2b28a..c8d5873 100644 --- a/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs +++ b/src/DotNetOpenAuth/Messaging/Bindings/StandardReplayProtectionBindingElement.cs @@ -28,11 +28,6 @@ namespace DotNetOpenAuth.Messaging.Bindings { private int nonceLength = 8; /// <summary> - /// A random number generator. - /// </summary> - private Random generator = new Random(); - - /// <summary> /// Initializes a new instance of the <see cref="StandardReplayProtectionBindingElement"/> class. /// </summary> /// <param name="nonceStore">The store where nonces will be persisted and checked.</param> @@ -145,11 +140,7 @@ namespace DotNetOpenAuth.Messaging.Bindings { /// </summary> /// <returns>The nonce string.</returns> private string GenerateUniqueFragment() { - char[] nonce = new char[this.nonceLength]; - for (int i = 0; i < nonce.Length; i++) { - nonce[i] = AllowedCharacters[this.generator.Next(AllowedCharacters.Length)]; - } - return new string(nonce); + return MessagingUtilities.GetRandomString(this.nonceLength, AllowedCharacters); } } } diff --git a/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs b/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs index 82910d5..22c660c 100644 --- a/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs +++ b/src/DotNetOpenAuth/Messaging/MessagePartAttribute.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.Messaging { using System; + using System.Diagnostics; using System.Net.Security; using System.Reflection; @@ -13,6 +14,7 @@ namespace DotNetOpenAuth.Messaging { /// Applied to fields and properties that form a key/value in a protocol message. /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = true)] + [DebuggerDisplay("MessagePartAttribute {Name}")] public sealed class MessagePartAttribute : Attribute { /// <summary> /// The overridden name to use as the serialized name for the property. diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index 10ddba0..78342ae 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -31,6 +31,32 @@ namespace DotNetOpenAuth.Messaging { internal static readonly RandomNumberGenerator CryptoRandomDataGenerator = new RNGCryptoServiceProvider(); /// <summary> + /// A pseudo-random data generator (NOT cryptographically strong random data) + /// </summary> + internal static readonly Random NonCryptoRandomDataGenerator = new Random(); + + /// <summary> + /// The uppercase alphabet. + /// </summary> + internal const string UppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /// <summary> + /// The lowercase alphabet. + /// </summary> + internal const string LowercaseLetters = "abcdefghijklmnopqrstuvwxyz"; + + /// <summary> + /// The set of base 10 digits. + /// </summary> + internal const string Digits = "0123456789"; + + /// <summary> + /// The set of digits, and alphabetic letters (upper and lowercase) that are clearly + /// visually distinguishable. + /// </summary> + internal const string AlphaNumericNoLookAlikes = "23456789abcdefghjkmnpqrstwxyzABCDEFGHJKMNPQRSTWXYZ"; + + /// <summary> /// The set of characters that are unreserved in RFC 2396 but are NOT unreserved in RFC 3986. /// </summary> private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" }; @@ -163,6 +189,24 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Gets a random string made up of a given set of allowable characters. + /// </summary> + /// <param name="length">The length of the desired random string.</param> + /// <param name="allowableCharacters">The allowable characters.</param> + /// <returns>A random string.</returns> + internal static string GetRandomString(int length, string allowableCharacters) { + Contract.Requires(length >= 0); + Contract.Requires(allowableCharacters != null && allowableCharacters.Length >= 2); + + char[] randomString = new char[length]; + for (int i = 0; i < length; i++) { + randomString[i] = allowableCharacters[NonCryptoRandomDataGenerator.Next(allowableCharacters.Length)]; + } + + return new string(randomString); + } + + /// <summary> /// Adds a set of HTTP headers to an <see cref="HttpResponse"/> instance, /// taking care to set some headers to the appropriate properties of /// <see cref="HttpResponse" /> diff --git a/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartNullEncoder.cs b/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartNullEncoder.cs new file mode 100644 index 0000000..7581550 --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/Reflection/IMessagePartNullEncoder.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="IMessagePartNullEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging.Reflection { + /// <summary> + /// A message part encoder that has a special encoding for a null value. + /// </summary> + public interface IMessagePartNullEncoder : IMessagePartEncoder { + /// <summary> + /// Gets the string representation to include in a serialized message + /// when the message part has a <c>null</c> value. + /// </summary> + string EncodedNullValue { get; } + } +} diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs index 0e26860..0732bb2 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Security; @@ -17,6 +18,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <summary> /// Describes an individual member of a message and assists in its serialization. /// </summary> + [DebuggerDisplay("MessagePart {Name}")] internal class MessagePart { /// <summary> /// A map of converters that help serialize custom objects to string values and back again. @@ -113,10 +115,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { str => str != null ? Convert.ChangeType(str, this.memberDeclaredType, CultureInfo.InvariantCulture) : null); } } else { - var encoder = GetEncoder(attribute.Encoder); - this.converter = new ValueMapping( - obj => encoder.Encode(obj), - str => encoder.Decode(str)); + this.converter = new ValueMapping(GetEncoder(attribute.Encoder)); } // readonly and const fields are considered legal, and "constants" for message transport. @@ -286,7 +285,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// An instance of the appropriate type for setting the member. /// </returns> private object ToValue(string value) { - return value == null ? null : this.converter.StringToValue(value); + return this.converter.StringToValue(value); } /// <summary> @@ -297,7 +296,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// The string representation of the member's value. /// </returns> private string ToString(object value) { - return value == null ? null : this.converter.ValueToString(value); + return this.converter.ValueToString(value); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs b/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs index bdc2d7f..332274e 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs @@ -14,12 +14,12 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <summary> /// The mapping function that converts some custom type to a string. /// </summary> - internal Func<object, string> ValueToString; + internal readonly Func<object, string> ValueToString; /// <summary> /// The mapping function that converts a string to some custom type. /// </summary> - internal Func<string, object> StringToValue; + internal readonly Func<string, object> StringToValue; /// <summary> /// Initializes a new instance of the <see cref="ValueMapping"/> struct. @@ -27,16 +27,24 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <param name="toString">The mapping function that converts some custom type to a string.</param> /// <param name="toValue">The mapping function that converts a string to some custom type.</param> internal ValueMapping(Func<object, string> toString, Func<string, object> toValue) { - if (toString == null) { - throw new ArgumentNullException("toString"); - } - - if (toValue == null) { - throw new ArgumentNullException("toValue"); - } + ErrorUtilities.VerifyArgumentNotNull(toString, "toString"); + ErrorUtilities.VerifyArgumentNotNull(toValue, "toValue"); this.ValueToString = toString; this.StringToValue = toValue; } + + /// <summary> + /// Initializes a new instance of the <see cref="ValueMapping"/> struct. + /// </summary> + /// <param name="encoder">The encoder.</param> + internal ValueMapping(IMessagePartEncoder encoder) { + ErrorUtilities.VerifyArgumentNotNull(encoder, "encoder"); + var nullEncoder = encoder as IMessagePartNullEncoder; + string nullString = nullEncoder != null ? nullEncoder.EncodedNullValue : null; + + this.ValueToString = obj => (obj != null) ? encoder.Encode(obj) : nullString; + this.StringToValue = str => (str != null) ? encoder.Decode(str) : null; + } } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/IConsumerCertificateProvider.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/IConsumerCertificateProvider.cs deleted file mode 100644 index 7e6ae54..0000000 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/IConsumerCertificateProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="IConsumerCertificateProvider.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.OAuth.ChannelElements { - using System.Security.Cryptography.X509Certificates; - - /// <summary> - /// A provider that hosts can implement to hook up their RSA-SHA1 binding elements - /// to their list of known Consumers' certificates. - /// </summary> - public interface IConsumerCertificateProvider { - /// <summary> - /// Gets the certificate that can be used to verify the signature of an incoming - /// message from a Consumer. - /// </summary> - /// <param name="consumerMessage">The incoming message from some Consumer.</param> - /// <returns>The public key from the Consumer's X.509 Certificate, if one can be found; otherwise <c>null</c>.</returns> - X509Certificate2 GetCertificate(ITamperResistantOAuthMessage consumerMessage); - } -} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/IConsumerDescription.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/IConsumerDescription.cs new file mode 100644 index 0000000..db505d5 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/IConsumerDescription.cs @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------- +// <copyright file="IConsumerDescription.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Security.Cryptography.X509Certificates; + + /// <summary> + /// A description of a consumer from a Service Provider's point of view. + /// </summary> + public interface IConsumerDescription { + /// <summary> + /// Gets the Consumer key. + /// </summary> + string Key { get; } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + string Secret { get; } + + /// <summary> + /// Gets the certificate that can be used to verify the signature of an incoming + /// message from a Consumer. + /// </summary> + /// <returns>The public key from the Consumer's X.509 Certificate, if one can be found; otherwise <c>null</c>.</returns> + /// <remarks> + /// This property must be implemented only if the RSA-SHA1 algorithm is supported by the Service Provider. + /// </remarks> + X509Certificate2 Certificate { get; } + + /// <summary> + /// Gets the callback URI that this consumer has pre-registered with the service provider, if any. + /// </summary> + /// <value>A URI that user authorization responses should be directed to; or <c>null</c> if no preregistered callback was arranged.</value> + Uri Callback { get; } + + /// <summary> + /// Gets the verification code format that is most appropriate for this consumer + /// when a callback URI is not available. + /// </summary> + /// <value>A set of characters that can be easily keyed in by the user given the Consumer's + /// application type and form factor.</value> + /// <remarks> + /// The value <see cref="OAuth.VerificationCodeFormat.IncludedInCallback"/> should NEVER be returned + /// since this property is only used in no callback scenarios anyway. + /// </remarks> + VerificationCodeFormat VerificationCodeFormat { get; } + + /// <summary> + /// Gets the length of the verification code to issue for this Consumer. + /// </summary> + /// <value>A positive number, generally at least 4.</value> + int VerificationCodeLength { get; } + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs new file mode 100644 index 0000000..48f39a6 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="IServiceProviderRequestToken.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// A description of a request token and its metadata as required by a Service Provider + /// </summary> + public interface IServiceProviderRequestToken { + /// <summary> + /// Gets the token itself. + /// </summary> + string Token { get; } + + /// <summary> + /// Gets the consumer key that requested this token. + /// </summary> + string ConsumerKey { get; } + + /// <summary> + /// Gets or sets the callback associated specifically with this token, if any. + /// </summary> + /// <value>The callback URI; or <c>null</c> if no callback was specifically assigned to this token.</value> + Uri Callback { get; set; } + + /// <summary> + /// Gets or sets the verifier that the consumer must include in the <see cref="AuthorizedTokenRequest"/> + /// message to exchange this request token for an access token. + /// </summary> + /// <value>The verifier code, or <c>null</c> if none has been assigned (yet).</value> + string VerificationCode { get; set; } + + /// <summary> + /// Gets or sets the version of the Consumer that requested this token. + /// </summary> + /// <remarks> + /// This property is used to determine whether a <see cref="VerificationCode"/> must be + /// generated when the user authorizes the Consumer or not. + /// </remarks> + Version ConsumerVersion { get; set; } + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs index e1c1e3f..fa008ac 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs @@ -16,12 +16,19 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> public interface IServiceProviderTokenManager : ITokenManager { /// <summary> - /// Gets the Consumer Secret for a given a Consumer Key. + /// Gets the Consumer description for a given a Consumer Key. /// </summary> /// <param name="consumerKey">The Consumer Key.</param> - /// <returns>The Consumer Secret.</returns> - /// <exception cref="ArgumentException">Thrown if the consumer key cannot be found.</exception> - /// <exception cref="InvalidOperationException">May be thrown if called when the signature algorithm does not require a consumer secret, such as when RSA-SHA1 is used.</exception> - string GetConsumerSecret(string consumerKey); + /// <returns>A description of the consumer. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the consumer key cannot be found.</exception> + IConsumerDescription GetConsumer(string consumerKey); + + /// <summary> + /// Gets details on the named request token. + /// </summary> + /// <param name="token">The request token.</param> + /// <returns>A description of the token. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the token cannot be found.</exception> + IServiceProviderRequestToken GetRequestToken(string token); } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index 5ac1aa8..2487845 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -63,7 +63,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <see cref="OAuthConsumerMessageFactory"/> or <see cref="OAuthServiceProviderMessageFactory"/>. /// </param> internal OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, IMessageFactory messageTypeProvider) - : base(messageTypeProvider, new OAuthHttpMethodBindingElement(), signingBindingElement, new StandardExpirationBindingElement(), new StandardReplayProtectionBindingElement(store)) { + : base(messageTypeProvider, InitializeBindingElements(signingBindingElement, store, tokenManager)) { ErrorUtilities.VerifyArgumentNotNull(tokenManager, "tokenManager"); this.TokenManager = tokenManager; @@ -122,7 +122,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { string authorization = request.Headers[HttpRequestHeader.Authorization]; if (authorization != null) { string[] authorizationSections = authorization.Split(';'); // TODO: is this the right delimiter? - string oauthPrefix = Protocol.Default.AuthorizationHeaderScheme + " "; + string oauthPrefix = Protocol.AuthorizationHeaderScheme + " "; // The Authorization header may have multiple uses, and OAuth may be just one of them. // Go through each one looking for an OAuth one. @@ -241,6 +241,29 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { } /// <summary> + /// Initializes the binding elements for the OAuth channel. + /// </summary> + /// <param name="signingBindingElement">The signing binding element.</param> + /// <param name="store">The nonce store.</param> + /// <param name="tokenManager">The token manager.</param> + /// <returns>An array of binding elements used to initialize the channel.</returns> + private static IChannelBindingElement[] InitializeBindingElements(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager) { + var bindingElements = new List<IChannelBindingElement> { + new OAuthHttpMethodBindingElement(), + signingBindingElement, + new StandardExpirationBindingElement(), + new StandardReplayProtectionBindingElement(store), + }; + + var spTokenManager = tokenManager as IServiceProviderTokenManager; + if (spTokenManager != null) { + bindingElements.Insert(0, new TokenHandlingBindingElement(spTokenManager)); + } + + return bindingElements.ToArray(); + } + + /// <summary> /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1. /// </summary> /// <param name="source">The dictionary with names and values to encode.</param> @@ -307,7 +330,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { httpRequest.Method = GetHttpMethod(requestMessage); StringBuilder authorization = new StringBuilder(); - authorization.Append(protocol.AuthorizationHeaderScheme); + authorization.Append(Protocol.AuthorizationHeaderScheme); authorization.Append(" "); foreach (var pair in fields) { string key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key); @@ -366,7 +389,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { ErrorUtilities.VerifyInternal(consumerKey == consumerTokenManager.ConsumerKey, "The token manager consumer key and the consumer key set earlier do not match!"); return consumerTokenManager.ConsumerSecret; } else { - return ((IServiceProviderTokenManager)this.TokenManager).GetConsumerSecret(consumerKey); + return ((IServiceProviderTokenManager)this.TokenManager).GetConsumer(consumerKey).Secret; } } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs index fce351b..e05bb62 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs @@ -44,7 +44,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { MessageBase message = null; if (fields.ContainsKey("oauth_token")) { - message = new UserAuthorizationResponse(recipient.Location); + Protocol protocol = fields.ContainsKey("oauth_verifier") ? Protocol.V10a : Protocol.V10; + message = new UserAuthorizationResponse(recipient.Location, protocol.Version); } if (message != null) { @@ -92,7 +93,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { var unauthorizedTokenRequest = request as UnauthorizedTokenRequest; var authorizedTokenRequest = request as AuthorizedTokenRequest; if (unauthorizedTokenRequest != null) { - message = new UnauthorizedTokenResponse(unauthorizedTokenRequest); + Protocol protocol = fields.ContainsKey("oauth_callback_confirmed") ? Protocol.V10a : Protocol.V10; + message = new UnauthorizedTokenResponse(unauthorizedTokenRequest, protocol.Version); } else if (authorizedTokenRequest != null) { message = new AuthorizedTokenResponse(authorizedTokenRequest); } else { diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs index 1aaea7f..4727a6d 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs @@ -54,29 +54,51 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { ErrorUtilities.VerifyArgumentNotNull(fields, "fields"); MessageBase message = null; + Protocol protocol = Protocol.V10; // default to assuming the less-secure 1.0 instead of 1.0a until we prove otherwise. + string token; + fields.TryGetValue("oauth_token", out token); - if (fields.ContainsKey("oauth_consumer_key") && - !fields.ContainsKey("oauth_token")) { - message = new UnauthorizedTokenRequest(recipient); - } else if (fields.ContainsKey("oauth_consumer_key") && - fields.ContainsKey("oauth_token")) { - // Discern between RequestAccessToken and AccessProtectedResources, - // which have all the same parameters, by figuring out what type of token - // is in the token parameter. - bool tokenTypeIsAccessToken = this.tokenManager.GetTokenType(fields["oauth_token"]) == TokenType.AccessToken; + try { + if (fields.ContainsKey("oauth_consumer_key") && !fields.ContainsKey("oauth_token")) { + protocol = fields.ContainsKey("oauth_callback") ? Protocol.V10a : Protocol.V10; + message = new UnauthorizedTokenRequest(recipient, protocol.Version); + } else if (fields.ContainsKey("oauth_consumer_key") && fields.ContainsKey("oauth_token")) { + // Discern between RequestAccessToken and AccessProtectedResources, + // which have all the same parameters, by figuring out what type of token + // is in the token parameter. + bool tokenTypeIsAccessToken = this.tokenManager.GetTokenType(token) == TokenType.AccessToken; - message = tokenTypeIsAccessToken ? (MessageBase)new AccessProtectedResourceRequest(recipient) : - new AuthorizedTokenRequest(recipient); - } else { - // fail over to the message with no required fields at all. - message = new UserAuthorizationRequest(recipient); - } + if (tokenTypeIsAccessToken) { + message = (MessageBase)new AccessProtectedResourceRequest(recipient, protocol.Version); + } else { + // Discern between 1.0 and 1.0a requests by checking on the consumer version we stored + // when the consumer first requested an unauthorized token. + protocol = Protocol.Lookup(this.tokenManager.GetRequestToken(token).ConsumerVersion); + message = new AuthorizedTokenRequest(recipient, protocol.Version); + } + } else { + // fail over to the message with no required fields at all. + if (token != null) { + protocol = Protocol.Lookup(this.tokenManager.GetRequestToken(token).ConsumerVersion); + } - if (message != null) { - message.SetAsIncoming(); - } + // If a callback parameter is included, that suggests either the consumer + // is following OAuth 1.0 instead of 1.0a, or that a hijacker is trying + // to attack. Either way, if the consumer started out as a 1.0a, keep it + // that way, and we'll just ignore the oauth_callback included in this message + // by virtue of the UserAuthorizationRequest message not including it in its + // 1.0a payload. + message = new UserAuthorizationRequest(recipient, protocol.Version); + } - return message; + if (message != null) { + message.SetAsIncoming(); + } + + return message; + } catch (KeyNotFoundException ex) { + throw ErrorUtilities.Wrap(ex, OAuthStrings.TokenNotFound); + } } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs index 779f2c5..4f8b5e5 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System; + using System.Diagnostics.Contracts; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -16,15 +17,24 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> public class RsaSha1SigningBindingElement : SigningBindingElementBase { /// <summary> + /// The name of the hash algorithm to use. + /// </summary> + private const string HashAlgorithmName = "RSA-SHA1"; + + /// <summary> + /// The token manager for the service provider. + /// </summary> + private IServiceProviderTokenManager tokenManager; + + /// <summary> /// Initializes a new instance of the <see cref="RsaSha1SigningBindingElement"/> class /// for use by Consumers. /// </summary> /// <param name="signingCertificate">The certificate used to sign outgoing messages.</param> public RsaSha1SigningBindingElement(X509Certificate2 signingCertificate) - : this() { - if (signingCertificate == null) { - throw new ArgumentNullException("signingCertificate"); - } + : base(HashAlgorithmName) { + Contract.Requires(signingCertificate != null); + ErrorUtilities.VerifyArgumentNotNull(signingCertificate, "signingCertificate"); this.SigningCertificate = signingCertificate; } @@ -33,21 +43,21 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// Initializes a new instance of the <see cref="RsaSha1SigningBindingElement"/> class /// for use by Service Providers. /// </summary> - public RsaSha1SigningBindingElement() - : base("RSA-SHA1") { + /// <param name="tokenManager">The token manager.</param> + public RsaSha1SigningBindingElement(IServiceProviderTokenManager tokenManager) + : base(HashAlgorithmName) { + Contract.Requires(tokenManager != null); + ErrorUtilities.VerifyArgumentNotNull(tokenManager, "tokenManager"); + + this.tokenManager = tokenManager; } /// <summary> - /// Gets or sets the certificate used to sign outgoing messages. + /// Gets or sets the certificate used to sign outgoing messages. Used only by Consumers. /// </summary> public X509Certificate2 SigningCertificate { get; set; } /// <summary> - /// Gets or sets the consumer certificate provider. - /// </summary> - public IConsumerCertificateProvider ConsumerCertificateProvider { get; set; } - - /// <summary> /// Calculates a signature for a given message. /// </summary> /// <param name="message">The message to sign.</param> @@ -56,13 +66,8 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// This method signs the message per OAuth 1.0 section 9.3. /// </remarks> protected override string GetSignature(ITamperResistantOAuthMessage message) { - if (message == null) { - throw new ArgumentNullException("message"); - } - - if (this.SigningCertificate == null) { - throw new InvalidOperationException(OAuthStrings.X509CertificateNotProvidedForSigning); - } + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + ErrorUtilities.VerifyOperation(this.SigningCertificate != null, OAuthStrings.X509CertificateNotProvidedForSigning); string signatureBaseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message)); byte[] data = Encoding.ASCII.GetBytes(signatureBaseString); @@ -80,16 +85,14 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>. /// </returns> protected override bool IsSignatureValid(ITamperResistantOAuthMessage message) { - if (this.ConsumerCertificateProvider == null) { - throw new InvalidOperationException(OAuthStrings.ConsumerCertificateProviderNotAvailable); - } + ErrorUtilities.VerifyInternal(this.tokenManager != null, "No token manager available for fetching Consumer public certificates."); string signatureBaseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message)); byte[] data = Encoding.ASCII.GetBytes(signatureBaseString); byte[] carriedSignature = Convert.FromBase64String(message.Signature); - X509Certificate2 cert = this.ConsumerCertificateProvider.GetCertificate(message); + X509Certificate2 cert = this.tokenManager.GetConsumer(message.ConsumerKey).Certificate; if (cert == null) { Logger.Signatures.WarnFormat("Incoming message from consumer '{0}' could not be matched with an appropriate X.509 certificate for signature verification.", message.ConsumerKey); return false; @@ -105,10 +108,11 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> /// <returns>A new instance of the binding element.</returns> protected override ITamperProtectionChannelBindingElement Clone() { - return new RsaSha1SigningBindingElement() { - ConsumerCertificateProvider = this.ConsumerCertificateProvider, - SigningCertificate = this.SigningCertificate, - }; + if (this.tokenManager != null) { + return new RsaSha1SigningBindingElement(this.tokenManager); + } else { + return new RsaSha1SigningBindingElement(this.SigningCertificate); + } } } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs new file mode 100644 index 0000000..b8273c3 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------- +// <copyright file="TokenHandlingBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// A binding element for Service Providers to manage the + /// callbacks and verification codes on applicable messages. + /// </summary> + internal class TokenHandlingBindingElement : IChannelBindingElement { + /// <summary> + /// The token manager offered by the service provider. + /// </summary> + private IServiceProviderTokenManager tokenManager; + + /// <summary> + /// Initializes a new instance of the <see cref="TokenHandlingBindingElement"/> class. + /// </summary> + /// <param name="tokenManager">The token manager.</param> + internal TokenHandlingBindingElement(IServiceProviderTokenManager tokenManager) { + Contract.Requires(tokenManager != null); + ErrorUtilities.VerifyArgumentNotNull(tokenManager, "tokenManager"); + + this.tokenManager = tokenManager; + } + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + + var userAuthResponse = message as UserAuthorizationResponse; + if (userAuthResponse != null && userAuthResponse.Version >= Protocol.V10a.Version) { + this.tokenManager.GetRequestToken(userAuthResponse.RequestToken).VerificationCode = userAuthResponse.VerificationCode; + return MessageProtections.None; + } + + // Hook to store the token and secret on its way down to the Consumer. + var grantRequestTokenResponse = message as UnauthorizedTokenResponse; + if (grantRequestTokenResponse != null) { + this.tokenManager.StoreNewRequestToken(grantRequestTokenResponse.RequestMessage, grantRequestTokenResponse); + this.tokenManager.GetRequestToken(grantRequestTokenResponse.RequestToken).ConsumerVersion = grantRequestTokenResponse.Version; + if (grantRequestTokenResponse.RequestMessage.Callback != null) { + this.tokenManager.GetRequestToken(grantRequestTokenResponse.RequestToken).Callback = grantRequestTokenResponse.RequestMessage.Callback; + } + + return MessageProtections.None; + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + + var authorizedTokenRequest = message as AuthorizedTokenRequest; + if (authorizedTokenRequest != null && authorizedTokenRequest.Version >= Protocol.V10a.Version) { + string expectedVerifier = this.tokenManager.GetRequestToken(authorizedTokenRequest.RequestToken).VerificationCode; + ErrorUtilities.VerifyProtocol(string.Equals(authorizedTokenRequest.VerificationCode, expectedVerifier, StringComparison.Ordinal), OAuthStrings.IncorrectVerifier); + return MessageProtections.None; + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/UriOrOobEncoding.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/UriOrOobEncoding.cs new file mode 100644 index 0000000..5aedc9d --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/UriOrOobEncoding.cs @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------- +// <copyright file="UriOrOobEncoding.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// An URI encoder that translates null <see cref="Uri"/> references as "oob" + /// instead of an empty/missing argument. + /// </summary> + internal class UriOrOobEncoding : IMessagePartNullEncoder { + /// <summary> + /// The string constant "oob", used to indicate an out-of-band configuration. + /// </summary> + private const string OutOfBandConfiguration = "oob"; + + /// <summary> + /// Initializes a new instance of the <see cref="UriOrOobEncoding"/> class. + /// </summary> + public UriOrOobEncoding() { + } + + #region IMessagePartNullEncoder Members + + /// <summary> + /// Gets the string representation to include in a serialized message + /// when the message part has a <c>null</c> value. + /// </summary> + /// <value></value> + public string EncodedNullValue { + get { return OutOfBandConfiguration; } + } + + #endregion + + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + ErrorUtilities.VerifyArgumentNotNull(value, "value"); + + Uri uriValue = (Uri)value; + return uriValue.AbsoluteUri; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + if (string.Equals(value, OutOfBandConfiguration, StringComparison.Ordinal)) { + return null; + } else { + return new Uri(value, UriKind.Absolute); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs index 2392172..8c4484b 100644 --- a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs +++ b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs @@ -10,6 +10,7 @@ namespace DotNetOpenAuth.OAuth { using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Net; + using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth.ChannelElements; @@ -32,6 +33,7 @@ namespace DotNetOpenAuth.OAuth { INonceStore store = new NonceMemoryStore(StandardExpirationBindingElement.DefaultMaximumMessageAge); this.OAuthChannel = new OAuthChannel(signingElement, store, tokenManager); this.ServiceProvider = serviceDescription; + this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.Consumer.SecuritySettings.CreateSecuritySettings(); } /// <summary> @@ -61,6 +63,11 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Gets the security settings for this consumer. + /// </summary> + internal ConsumerSecuritySettings SecuritySettings { get; private set; } + + /// <summary> /// Gets or sets the channel to use for sending/receiving messages. /// </summary> internal OAuthChannel OAuthChannel { get; set; } @@ -167,7 +174,7 @@ namespace DotNetOpenAuth.OAuth { ErrorUtilities.VerifyArgumentNotNull(endpoint, "endpoint"); ErrorUtilities.VerifyNonZeroLength(accessToken, "accessToken"); - AccessProtectedResourceRequest message = new AccessProtectedResourceRequest(endpoint) { + AccessProtectedResourceRequest message = new AccessProtectedResourceRequest(endpoint, this.ServiceProvider.Version) { AccessToken = accessToken, ConsumerKey = this.ConsumerKey, }; @@ -189,18 +196,26 @@ namespace DotNetOpenAuth.OAuth { /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "3#", Justification = "Two results")] protected internal UserAuthorizationRequest PrepareRequestUserAuthorization(Uri callback, IDictionary<string, string> requestParameters, IDictionary<string, string> redirectParameters, out string requestToken) { - // Obtain an unauthorized request token. - var token = new UnauthorizedTokenRequest(this.ServiceProvider.RequestTokenEndpoint) { + // Obtain an unauthorized request token. Assume the OAuth version given in the service description. + var token = new UnauthorizedTokenRequest(this.ServiceProvider.RequestTokenEndpoint, this.ServiceProvider.Version) { ConsumerKey = this.ConsumerKey, + Callback = callback, }; var tokenAccessor = this.Channel.MessageDescriptions.GetAccessor(token); tokenAccessor.AddExtraParameters(requestParameters); var requestTokenResponse = this.Channel.Request<UnauthorizedTokenResponse>(token); this.TokenManager.StoreNewRequestToken(token, requestTokenResponse); - // Request user authorization. + // Fine-tune our understanding of the SP's supported OAuth version if it's wrong. + if (this.ServiceProvider.Version != requestTokenResponse.Version) { + Logger.OAuth.WarnFormat("Expected OAuth service provider at endpoint {0} to use OAuth {1} but {2} was detected. Adjusting service description to new version.", this.ServiceProvider.RequestTokenEndpoint, this.ServiceProvider.Version, requestTokenResponse.Version); + this.ServiceProvider.ProtocolVersion = Protocol.Lookup(requestTokenResponse.Version).ProtocolVersion; + } + + // Request user authorization. The OAuth version will automatically include + // or drop the callback that we're setting here. ITokenContainingMessage assignedRequestToken = requestTokenResponse; - var requestAuthorization = new UserAuthorizationRequest(this.ServiceProvider.UserAuthorizationEndpoint, assignedRequestToken.Token) { + var requestAuthorization = new UserAuthorizationRequest(this.ServiceProvider.UserAuthorizationEndpoint, assignedRequestToken.Token, requestTokenResponse.Version) { Callback = callback, }; var requestAuthorizationAccessor = this.Channel.MessageDescriptions.GetAccessor(requestAuthorization); @@ -213,10 +228,14 @@ namespace DotNetOpenAuth.OAuth { /// Exchanges a given request token for access token. /// </summary> /// <param name="requestToken">The request token that the user has authorized.</param> - /// <returns>The access token assigned by the Service Provider.</returns> - protected AuthorizedTokenResponse ProcessUserAuthorization(string requestToken) { - var requestAccess = new AuthorizedTokenRequest(this.ServiceProvider.AccessTokenEndpoint) { + /// <param name="verifier">The verifier code.</param> + /// <returns> + /// The access token assigned by the Service Provider. + /// </returns> + protected AuthorizedTokenResponse ProcessUserAuthorization(string requestToken, string verifier) { + var requestAccess = new AuthorizedTokenRequest(this.ServiceProvider.AccessTokenEndpoint, this.ServiceProvider.Version) { RequestToken = requestToken, + VerificationCode = verifier, ConsumerKey = this.ConsumerKey, }; var grantAccess = this.Channel.Request<AuthorizedTokenResponse>(requestAccess); diff --git a/src/DotNetOpenAuth/OAuth/ConsumerSecuritySettings.cs b/src/DotNetOpenAuth/OAuth/ConsumerSecuritySettings.cs new file mode 100644 index 0000000..bb2fbaa --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ConsumerSecuritySettings.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="ConsumerSecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + /// <summary> + /// Security settings that are applicable to consumers. + /// </summary> + internal class ConsumerSecuritySettings : SecuritySettings { + /// <summary> + /// Initializes a new instance of the <see cref="ConsumerSecuritySettings"/> class. + /// </summary> + internal ConsumerSecuritySettings() { + } + } +} diff --git a/src/DotNetOpenAuth/OAuth/DesktopConsumer.cs b/src/DotNetOpenAuth/OAuth/DesktopConsumer.cs index ca74a77..f9c1a94 100644 --- a/src/DotNetOpenAuth/OAuth/DesktopConsumer.cs +++ b/src/DotNetOpenAuth/OAuth/DesktopConsumer.cs @@ -49,8 +49,25 @@ namespace DotNetOpenAuth.OAuth { /// </summary> /// <param name="requestToken">The request token that the user has authorized.</param> /// <returns>The access token assigned by the Service Provider.</returns> - public new AuthorizedTokenResponse ProcessUserAuthorization(string requestToken) { - return base.ProcessUserAuthorization(requestToken); + [Obsolete("Use the ProcessUserAuthorization method that takes a verifier parameter instead.")] + public AuthorizedTokenResponse ProcessUserAuthorization(string requestToken) { + return this.ProcessUserAuthorization(requestToken, null); + } + + /// <summary> + /// Exchanges a given request token for access token. + /// </summary> + /// <param name="requestToken">The request token that the user has authorized.</param> + /// <param name="verifier">The verifier code typed in by the user. Must not be <c>Null</c> for OAuth 1.0a service providers and later.</param> + /// <returns> + /// The access token assigned by the Service Provider. + /// </returns> + public new AuthorizedTokenResponse ProcessUserAuthorization(string requestToken, string verifier) { + if (this.ServiceProvider.Version >= Protocol.V10a.Version) { + ErrorUtilities.VerifyNonZeroLength(verifier, "verifier"); + } + + return base.ProcessUserAuthorization(requestToken, verifier); } } } diff --git a/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs b/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs index 62e02de..b60fda4 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth.Messages { + using System; using System.Diagnostics.CodeAnalysis; using DotNetOpenAuth.Messaging; @@ -17,8 +18,9 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Initializes a new instance of the <see cref="AccessProtectedResourceRequest"/> class. /// </summary> /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> - protected internal AccessProtectedResourceRequest(MessageReceivingEndpoint serviceProvider) - : base(MessageTransport.Direct, serviceProvider) { + /// <param name="version">The OAuth version.</param> + protected internal AccessProtectedResourceRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenRequest.cs b/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenRequest.cs index 2d4793c..1228290 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenRequest.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenRequest.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth.Messages { + using System; using System.Globalization; using DotNetOpenAuth.Messaging; @@ -20,8 +21,9 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Initializes a new instance of the <see cref="AuthorizedTokenRequest"/> class. /// </summary> /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> - internal AuthorizedTokenRequest(MessageReceivingEndpoint serviceProvider) - : base(MessageTransport.Direct, serviceProvider) { + /// <param name="version">The OAuth version.</param> + internal AuthorizedTokenRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { } /// <summary> @@ -33,6 +35,13 @@ namespace DotNetOpenAuth.OAuth.Messages { } /// <summary> + /// Gets or sets the verification code received by the Consumer from the Service Provider + /// in the <see cref="UserAuthorizationResponse.VerificationCode"/> property. + /// </summary> + [MessagePart("oauth_verifier", IsRequired = true, AllowEmpty = false, MinVersion = Protocol.V10aVersion)] + public string VerificationCode { get; set; } + + /// <summary> /// Gets or sets the unauthorized Request Token used to obtain authorization. /// </summary> [MessagePart("oauth_token", IsRequired = true)] diff --git a/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenResponse.cs b/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenResponse.cs index 14413a5..0b14819 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenResponse.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/AuthorizedTokenResponse.cs @@ -5,6 +5,7 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth.Messages { + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using DotNetOpenAuth.Messaging; @@ -19,7 +20,7 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </summary> /// <param name="originatingRequest">The originating request.</param> protected internal AuthorizedTokenResponse(AuthorizedTokenRequest originatingRequest) - : base(MessageProtections.None, originatingRequest) { + : base(MessageProtections.None, originatingRequest, originatingRequest.Version) { } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth/Messages/MessageBase.cs b/src/DotNetOpenAuth/OAuth/Messages/MessageBase.cs index e0269db..89e0276 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/MessageBase.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/MessageBase.cs @@ -62,12 +62,15 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </summary> /// <param name="protectionRequired">The level of protection the message requires.</param> /// <param name="originatingRequest">The request that asked for this direct response.</param> - protected MessageBase(MessageProtections protectionRequired, IDirectedProtocolMessage originatingRequest) { + /// <param name="version">The OAuth version.</param> + protected MessageBase(MessageProtections protectionRequired, IDirectedProtocolMessage originatingRequest, Version version) { ErrorUtilities.VerifyArgumentNotNull(originatingRequest, "originatingRequest"); + ErrorUtilities.VerifyArgumentNotNull(version, "version"); this.protectionRequired = protectionRequired; this.transport = MessageTransport.Direct; this.originatingRequest = originatingRequest; + this.Version = version; } /// <summary> @@ -76,14 +79,15 @@ namespace DotNetOpenAuth.OAuth.Messages { /// <param name="protectionRequired">The level of protection the message requires.</param> /// <param name="transport">A value indicating whether this message requires a direct or indirect transport.</param> /// <param name="recipient">The URI that a directed message will be delivered to.</param> - protected MessageBase(MessageProtections protectionRequired, MessageTransport transport, MessageReceivingEndpoint recipient) { - if (recipient == null) { - throw new ArgumentNullException("recipient"); - } + /// <param name="version">The OAuth version.</param> + protected MessageBase(MessageProtections protectionRequired, MessageTransport transport, MessageReceivingEndpoint recipient, Version version) { + ErrorUtilities.VerifyArgumentNotNull(recipient, "recipient"); + ErrorUtilities.VerifyArgumentNotNull(version, "version"); this.protectionRequired = protectionRequired; this.transport = transport; this.recipient = recipient; + this.Version = version; } #region IProtocolMessage Properties @@ -163,9 +167,7 @@ namespace DotNetOpenAuth.OAuth.Messages { /// <summary> /// Gets the version of the protocol this message is prepared to implement. /// </summary> - protected virtual Version Version { - get { return new Version(1, 0); } - } + protected internal Version Version { get; private set; } /// <summary> /// Gets the level of protection this message requires. diff --git a/src/DotNetOpenAuth/OAuth/Messages/SignedMessageBase.cs b/src/DotNetOpenAuth/OAuth/Messages/SignedMessageBase.cs index d1abb58..1d8ca21 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/SignedMessageBase.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/SignedMessageBase.cs @@ -31,8 +31,9 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </summary> /// <param name="transport">A value indicating whether this message requires a direct or indirect transport.</param> /// <param name="recipient">The URI that a directed message will be delivered to.</param> - internal SignedMessageBase(MessageTransport transport, MessageReceivingEndpoint recipient) - : base(MessageProtections.All, transport, recipient) { + /// <param name="version">The OAuth version.</param> + internal SignedMessageBase(MessageTransport transport, MessageReceivingEndpoint recipient, Version version) + : base(MessageProtections.All, transport, recipient, version) { ITamperResistantOAuthMessage self = (ITamperResistantOAuthMessage)this; HttpDeliveryMethods methods = ((IDirectedProtocolMessage)this).HttpMethods; self.HttpMethod = (methods & HttpDeliveryMethods.PostRequest) != 0 ? "POST" : "GET"; @@ -164,7 +165,7 @@ namespace DotNetOpenAuth.OAuth.Messages { [MessagePart("oauth_version", IsRequired = false)] private string OAuthVersion { get { - return Version.ToString(); + return Protocol.Lookup(Version).PublishedVersion; } set { diff --git a/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenRequest.cs b/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenRequest.cs index e491bad..9214d91 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenRequest.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenRequest.cs @@ -5,8 +5,10 @@ //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth.Messages { + using System; using System.Collections.Generic; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.ChannelElements; /// <summary> /// A direct message sent from Consumer to Service Provider to request a Request Token. @@ -16,11 +18,23 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Initializes a new instance of the <see cref="UnauthorizedTokenRequest"/> class. /// </summary> /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> - protected internal UnauthorizedTokenRequest(MessageReceivingEndpoint serviceProvider) - : base(MessageTransport.Direct, serviceProvider) { + /// <param name="version">The OAuth version.</param> + protected internal UnauthorizedTokenRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { } /// <summary> + /// Gets or sets the absolute URL to which the Service Provider will redirect the + /// User back when the Obtaining User Authorization step is completed. + /// </summary> + /// <value> + /// The callback URL; or <c>null</c> if the Consumer is unable to receive + /// callbacks or a callback URL has been established via other means. + /// </value> + [MessagePart("oauth_callback", IsRequired = true, AllowEmpty = false, MinVersion = Protocol.V10aVersion, Encoder = typeof(UriOrOobEncoding))] + public Uri Callback { get; set; } + + /// <summary> /// Gets the extra, non-OAuth parameters that will be included in the message. /// </summary> public new IDictionary<string, string> ExtraData { diff --git a/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenResponse.cs b/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenResponse.cs index 285dec7..8ccd8e3 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenResponse.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenResponse.cs @@ -25,7 +25,7 @@ namespace DotNetOpenAuth.OAuth.Messages { /// This constructor is used by the Service Provider to send the message. /// </remarks> protected internal UnauthorizedTokenResponse(UnauthorizedTokenRequest requestMessage, string requestToken, string tokenSecret) - : this(requestMessage) { + : this(requestMessage, requestMessage.Version) { ErrorUtilities.VerifyArgumentNotNull(requestToken, "requestToken"); ErrorUtilities.VerifyArgumentNotNull(tokenSecret, "tokenSecret"); @@ -37,9 +37,10 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Initializes a new instance of the <see cref="UnauthorizedTokenResponse"/> class. /// </summary> /// <param name="originatingRequest">The originating request.</param> + /// <param name="version">The OAuth version.</param> /// <remarks>This constructor is used by the consumer to deserialize the message.</remarks> - protected internal UnauthorizedTokenResponse(UnauthorizedTokenRequest originatingRequest) - : base(MessageProtections.None, originatingRequest) { + protected internal UnauthorizedTokenResponse(UnauthorizedTokenRequest originatingRequest, Version version) + : base(MessageProtections.None, originatingRequest, version) { } /// <summary> @@ -84,5 +85,13 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </summary> [MessagePart("oauth_token_secret", IsRequired = true)] protected internal string TokenSecret { get; set; } + + /// <summary> + /// Gets a value indicating whether the Service Provider recognized the callback parameter in the request. + /// </summary> + [MessagePart("oauth_callback_confirmed", IsRequired = true, MinVersion = Protocol.V10aVersion)] + private bool CallbackConfirmed { + get { return true; } + } } } diff --git a/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationRequest.cs b/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationRequest.cs index f1af0bc..099729e 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationRequest.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationRequest.cs @@ -21,8 +21,9 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </summary> /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> /// <param name="requestToken">The request token.</param> - internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, string requestToken) - : this(serviceProvider) { + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, string requestToken, Version version) + : this(serviceProvider, version) { this.RequestToken = requestToken; } @@ -30,8 +31,9 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Initializes a new instance of the <see cref="UserAuthorizationRequest"/> class. /// </summary> /// <param name="serviceProvider">The URI of the Service Provider endpoint to send this message to.</param> - internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider) - : base(MessageProtections.None, MessageTransport.Indirect, serviceProvider) { + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageProtections.None, MessageTransport.Indirect, serviceProvider, version) { } /// <summary> @@ -51,6 +53,14 @@ namespace DotNetOpenAuth.OAuth.Messages { } /// <summary> + /// Gets a value indicating whether this is a safe OAuth authorization request. + /// </summary> + /// <value><c>true</c> if the Consumer is using OAuth 1.0a or later; otherwise, <c>false</c>.</value> + public bool IsUnsafeRequest { + get { return this.Version < Protocol.V10a.Version; } + } + + /// <summary> /// Gets or sets the Request Token obtained in the previous step. /// </summary> /// <remarks> @@ -65,7 +75,7 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Gets or sets a URL the Service Provider will use to redirect the User back /// to the Consumer when Obtaining User Authorization is complete. Optional. /// </summary> - [MessagePart("oauth_callback", IsRequired = false)] + [MessagePart("oauth_callback", IsRequired = false, MaxVersion = "1.0")] internal Uri Callback { get; set; } } } diff --git a/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationResponse.cs b/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationResponse.cs index da6a909..69a327c 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationResponse.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/UserAuthorizationResponse.cs @@ -19,8 +19,9 @@ namespace DotNetOpenAuth.OAuth.Messages { /// Initializes a new instance of the <see cref="UserAuthorizationResponse"/> class. /// </summary> /// <param name="consumer">The URI of the Consumer endpoint to send this message to.</param> - internal UserAuthorizationResponse(Uri consumer) - : base(MessageProtections.None, MessageTransport.Indirect, new MessageReceivingEndpoint(consumer, HttpDeliveryMethods.GetRequest)) { + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationResponse(Uri consumer, Version version) + : base(MessageProtections.None, MessageTransport.Indirect, new MessageReceivingEndpoint(consumer, HttpDeliveryMethods.GetRequest), version) { } /// <summary> @@ -32,6 +33,20 @@ namespace DotNetOpenAuth.OAuth.Messages { } /// <summary> + /// Gets or sets the verification code that must accompany the request to exchange the + /// authorized request token for an access token. + /// </summary> + /// <value>An unguessable value passed to the Consumer via the User and REQUIRED to complete the process.</value> + /// <remarks> + /// If the Consumer did not provide a callback URL, the Service Provider SHOULD display the value of the + /// verification code, and instruct the User to manually inform the Consumer that authorization is + /// completed. If the Service Provider knows a Consumer to be running on a mobile device or set-top box, + /// the Service Provider SHOULD ensure that the verifier value is suitable for manual entry. + /// </remarks> + [MessagePart("oauth_verifier", IsRequired = true, AllowEmpty = false, MinVersion = Protocol.V10aVersion)] + public string VerificationCode { get; set; } + + /// <summary> /// Gets or sets the Request Token. /// </summary> [MessagePart("oauth_token", IsRequired = true)] diff --git a/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs b/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs index 63e348a..689998a 100644 --- a/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs +++ b/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:2.0.50727.3521 +// Runtime Version:2.0.50727.4918 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -79,20 +79,20 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> - /// Looks up a localized string similar to The RSA-SHA1 signing binding element's consumer certificate provider has not been set, so no incoming messages from consumers using this signature method can be verified.. + /// Looks up a localized string similar to Failure looking up secret for consumer or token.. /// </summary> - internal static string ConsumerCertificateProviderNotAvailable { + internal static string ConsumerOrTokenSecretNotFound { get { - return ResourceManager.GetString("ConsumerCertificateProviderNotAvailable", resourceCulture); + return ResourceManager.GetString("ConsumerOrTokenSecretNotFound", resourceCulture); } } /// <summary> - /// Looks up a localized string similar to Failure looking up secret for consumer or token.. + /// Looks up a localized string similar to oauth_verifier argument was incorrect.. /// </summary> - internal static string ConsumerOrTokenSecretNotFound { + internal static string IncorrectVerifier { get { - return ResourceManager.GetString("ConsumerOrTokenSecretNotFound", resourceCulture); + return ResourceManager.GetString("IncorrectVerifier", resourceCulture); } } @@ -115,6 +115,15 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Looks up a localized string similar to This OAuth service provider requires OAuth consumers to implement OAuth {0}, but this consumer appears to only support {1}.. + /// </summary> + internal static string MinimumConsumerVersionRequirementNotMet { + get { + return ResourceManager.GetString("MinimumConsumerVersionRequirementNotMet", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The request URL query MUST NOT contain any OAuth Protocol Parameters.. /// </summary> internal static string RequestUrlMustNotHaveOAuthParameters { @@ -142,6 +151,15 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Looks up a localized string similar to A token in the message was not recognized by the service provider.. + /// </summary> + internal static string TokenNotFound { + get { + return ResourceManager.GetString("TokenNotFound", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The RSA-SHA1 signing binding element has not been set with a certificate for signing.. /// </summary> internal static string X509CertificateNotProvidedForSigning { diff --git a/src/DotNetOpenAuth/OAuth/OAuthStrings.resx b/src/DotNetOpenAuth/OAuth/OAuthStrings.resx index 3ba4da1..a40b35d 100644 --- a/src/DotNetOpenAuth/OAuth/OAuthStrings.resx +++ b/src/DotNetOpenAuth/OAuth/OAuthStrings.resx @@ -123,18 +123,21 @@ <data name="BadAccessTokenInProtectedResourceRequest" xml:space="preserve"> <value>The access token '{0}' is invalid or expired.</value> </data> - <data name="ConsumerCertificateProviderNotAvailable" xml:space="preserve"> - <value>The RSA-SHA1 signing binding element's consumer certificate provider has not been set, so no incoming messages from consumers using this signature method can be verified.</value> - </data> <data name="ConsumerOrTokenSecretNotFound" xml:space="preserve"> <value>Failure looking up secret for consumer or token.</value> </data> + <data name="IncorrectVerifier" xml:space="preserve"> + <value>oauth_verifier argument was incorrect.</value> + </data> <data name="InvalidIncomingMessage" xml:space="preserve"> <value>An invalid OAuth message received and discarded.</value> </data> <data name="MessageNotAllowedExtraParameters" xml:space="preserve"> <value>The {0} message included extra data which is not allowed.</value> </data> + <data name="MinimumConsumerVersionRequirementNotMet" xml:space="preserve"> + <value>This OAuth service provider requires OAuth consumers to implement OAuth {0}, but this consumer appears to only support {1}.</value> + </data> <data name="RequestUrlMustNotHaveOAuthParameters" xml:space="preserve"> <value>The request URL query MUST NOT contain any OAuth Protocol Parameters.</value> </data> @@ -144,6 +147,9 @@ <data name="SigningElementsMustShareSameProtection" xml:space="preserve"> <value>All signing elements must offer the same message protection.</value> </data> + <data name="TokenNotFound" xml:space="preserve"> + <value>A token in the message was not recognized by the service provider.</value> + </data> <data name="X509CertificateNotProvidedForSigning" xml:space="preserve"> <value>The RSA-SHA1 signing binding element has not been set with a certificate for signing.</value> </data> diff --git a/src/DotNetOpenAuth/OAuth/Protocol.cs b/src/DotNetOpenAuth/OAuth/Protocol.cs index 88615ff..f535b10 100644 --- a/src/DotNetOpenAuth/OAuth/Protocol.cs +++ b/src/DotNetOpenAuth/OAuth/Protocol.cs @@ -7,17 +7,34 @@ namespace DotNetOpenAuth.OAuth { using System; using System.Collections.Generic; + using System.Diagnostics; using System.Linq; using System.Text; using DotNetOpenAuth.Messaging; /// <summary> + /// An enumeration of the OAuth protocol versions supported by this library. + /// </summary> + public enum ProtocolVersion { + /// <summary> + /// OAuth 1.0 specification + /// </summary> + V10, + + /// <summary> + /// OAuth 1.0a specification + /// </summary> + V10a, + } + + /// <summary> /// Constants used in the OAuth protocol. /// </summary> /// <remarks> /// OAuth Protocol Parameter names and values are case sensitive. Each OAuth Protocol Parameters MUST NOT appear more than once per request, and are REQUIRED unless otherwise noted, /// per OAuth 1.0 section 5. /// </remarks> + [DebuggerDisplay("OAuth {Version}")] internal class Protocol { /// <summary> /// The namespace to use for V1.0 of the protocol. @@ -25,63 +42,105 @@ namespace DotNetOpenAuth.OAuth { internal const string DataContractNamespaceV10 = "http://oauth.net/core/1.0/"; /// <summary> + /// The prefix used for all key names in the protocol. + /// </summary> + internal const string ParameterPrefix = "oauth_"; + + /// <summary> + /// The string representation of a <see cref="Version"/> instance to be used to represent OAuth 1.0a. + /// </summary> + internal const string V10aVersion = "1.0.1"; + + /// <summary> + /// The scheme to use in Authorization header message requests. + /// </summary> + internal const string AuthorizationHeaderScheme = "OAuth"; + + /// <summary> /// Gets the <see cref="Protocol"/> instance with values initialized for V1.0 of the protocol. /// </summary> internal static readonly Protocol V10 = new Protocol { dataContractNamespace = DataContractNamespaceV10, + Version = new Version(1, 0), + ProtocolVersion = ProtocolVersion.V10, }; /// <summary> + /// Gets the <see cref="Protocol"/> instance with values initialized for V1.0a of the protocol. + /// </summary> + internal static readonly Protocol V10a = new Protocol { + dataContractNamespace = DataContractNamespaceV10, + Version = new Version(V10aVersion), + ProtocolVersion = ProtocolVersion.V10a, + }; + + /// <summary> + /// A list of all supported OAuth versions, in order starting from newest version. + /// </summary> + internal static readonly List<Protocol> AllVersions = new List<Protocol>() { V10a, V10 }; + + /// <summary> + /// The default (or most recent) supported version of the OpenID protocol. + /// </summary> + internal static readonly Protocol Default = AllVersions[0]; + + /// <summary> /// The namespace to use for this version of the protocol. /// </summary> private string dataContractNamespace; /// <summary> - /// The prefix used for all key names in the protocol. + /// Initializes a new instance of the <see cref="Protocol"/> class. /// </summary> - private string parameterPrefix = "oauth_"; + internal Protocol() { + this.PublishedVersion = "1.0"; + } /// <summary> - /// The scheme to use in Authorization header message requests. + /// Gets the version used to represent OAuth 1.0a. /// </summary> - private string authorizationHeaderScheme = "OAuth"; + internal Version Version { get; private set; } /// <summary> - /// Gets the default <see cref="Protocol"/> instance. + /// Gets the version to declare on the wire. /// </summary> - internal static Protocol Default { get { return V10; } } + internal string PublishedVersion { get; private set; } /// <summary> - /// Gets the namespace to use for this version of the protocol. + /// Gets the <see cref="ProtocolVersion"/> enum value for the <see cref="Protocol"/> instance. /// </summary> - internal string DataContractNamespace { - get { return this.dataContractNamespace; } - } + internal ProtocolVersion ProtocolVersion { get; private set; } /// <summary> - /// Gets the prefix used for all key names in the protocol. + /// Gets the namespace to use for this version of the protocol. /// </summary> - internal string ParameterPrefix { - get { return this.parameterPrefix; } + internal string DataContractNamespace { + get { return this.dataContractNamespace; } } /// <summary> - /// Gets the scheme to use in Authorization header message requests. + /// Gets the OAuth Protocol instance to use for the given version. /// </summary> - internal string AuthorizationHeaderScheme { - get { return this.authorizationHeaderScheme; } + /// <param name="version">The OAuth version to get.</param> + /// <returns>A matching <see cref="Protocol"/> instance.</returns> + public static Protocol Lookup(ProtocolVersion version) { + switch (version) { + case ProtocolVersion.V10: return Protocol.V10; + case ProtocolVersion.V10a: return Protocol.V10a; + default: throw new ArgumentOutOfRangeException("version"); + } } /// <summary> - /// Gets an instance of <see cref="Protocol"/> given a <see cref="Version"/>. + /// Gets the OAuth Protocol instance to use for the given version. /// </summary> - /// <param name="version">The version of the protocol that is desired.</param> - /// <returns>The <see cref="Protocol"/> instance representing the requested version.</returns> + /// <param name="version">The OAuth version to get.</param> + /// <returns>A matching <see cref="Protocol"/> instance.</returns> internal static Protocol Lookup(Version version) { - switch (version.Major) { - case 1: return Protocol.V10; - default: throw new ArgumentOutOfRangeException("version"); - } + ErrorUtilities.VerifyArgumentNotNull(version, "version"); + Protocol protocol = AllVersions.FirstOrDefault(p => p.Version == version); + ErrorUtilities.VerifyArgumentInRange(protocol != null, "version"); + return protocol; } } } diff --git a/src/DotNetOpenAuth/OAuth/SecuritySettings.cs b/src/DotNetOpenAuth/OAuth/SecuritySettings.cs new file mode 100644 index 0000000..3329f09 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/SecuritySettings.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// <copyright file="SecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + /// <summary> + /// Security settings that may be applicable to both consumers and service providers. + /// </summary> + public class SecuritySettings { + /// <summary> + /// Initializes a new instance of the <see cref="SecuritySettings"/> class. + /// </summary> + protected SecuritySettings() { + } + } +} diff --git a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs index 345c6a2..d4fe85e 100644 --- a/src/DotNetOpenAuth/OAuth/ServiceProvider.cs +++ b/src/DotNetOpenAuth/OAuth/ServiceProvider.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OAuth { using System.Globalization; using System.ServiceModel.Channels; using System.Web; + using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth.ChannelElements; @@ -29,6 +30,11 @@ namespace DotNetOpenAuth.OAuth { /// </remarks> public class ServiceProvider : IDisposable { /// <summary> + /// The length of the verifier code (in raw bytes before base64 encoding) to generate. + /// </summary> + private const int VerifierCodeLength = 5; + + /// <summary> /// The field behind the <see cref="OAuthChannel"/> property. /// </summary> private OAuthChannel channel; @@ -48,7 +54,7 @@ namespace DotNetOpenAuth.OAuth { /// <param name="serviceDescription">The endpoints and behavior on the Service Provider.</param> /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> /// <param name="messageTypeProvider">An object that can figure out what type of message is being received for deserialization.</param> - public ServiceProvider(ServiceProviderDescription serviceDescription, ITokenManager tokenManager, OAuthServiceProviderMessageFactory messageTypeProvider) { + public ServiceProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, OAuthServiceProviderMessageFactory messageTypeProvider) { ErrorUtilities.VerifyArgumentNotNull(serviceDescription, "serviceDescription"); ErrorUtilities.VerifyArgumentNotNull(tokenManager, "tokenManager"); ErrorUtilities.VerifyArgumentNotNull(messageTypeProvider, "messageTypeProvider"); @@ -58,6 +64,7 @@ namespace DotNetOpenAuth.OAuth { this.ServiceDescription = serviceDescription; this.OAuthChannel = new OAuthChannel(signingElement, store, tokenManager, messageTypeProvider); this.TokenGenerator = new StandardTokenGenerator(); + this.SecuritySettings = DotNetOpenAuthSection.Configuration.OAuth.ServiceProvider.SecuritySettings.CreateSecuritySettings(); } /// <summary> @@ -73,8 +80,8 @@ namespace DotNetOpenAuth.OAuth { /// <summary> /// Gets the persistence store for tokens and secrets. /// </summary> - public ITokenManager TokenManager { - get { return this.OAuthChannel.TokenManager; } + public IServiceProviderTokenManager TokenManager { + get { return (IServiceProviderTokenManager)this.OAuthChannel.TokenManager; } } /// <summary> @@ -85,6 +92,11 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Gets the security settings for this service provider. + /// </summary> + public ServiceProviderSecuritySettings SecuritySettings { get; private set; } + + /// <summary> /// Gets or sets the channel to use for sending/receiving messages. /// </summary> internal OAuthChannel OAuthChannel { @@ -93,15 +105,38 @@ namespace DotNetOpenAuth.OAuth { } set { - if (this.channel != null) { - this.channel.Sending -= this.OAuthChannel_Sending; - } - + Contract.Requires(value != null); + ErrorUtilities.VerifyArgumentNotNull(value, "value"); this.channel = value; + } + } - if (this.channel != null) { - this.channel.Sending += this.OAuthChannel_Sending; - } + /// <summary> + /// Creates a cryptographically strong random verification code. + /// </summary> + /// <param name="format">The desired format of the verification code.</param> + /// <param name="length">The length of the code. + /// When <paramref name="format"/> is <see cref="VerificationCodeFormat.IncludedInCallback"/>, + /// this is the length of the original byte array before base64 encoding rather than the actual + /// length of the final string.</param> + /// <returns>The verification code.</returns> + public static string CreateVerificationCode(VerificationCodeFormat format, int length) { + Contract.Requires(length >= 0); + ErrorUtilities.VerifyArgumentInRange(length >= 0, "length"); + + switch (format) { + case VerificationCodeFormat.IncludedInCallback: + return MessagingUtilities.GetCryptoRandomDataAsBase64(length); + case VerificationCodeFormat.AlphaNumericNoLookAlikes: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.AlphaNumericNoLookAlikes); + case VerificationCodeFormat.AlphaUpper: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.UppercaseLetters); + case VerificationCodeFormat.AlphaLower: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.LowercaseLetters); + case VerificationCodeFormat.Numeric: + return MessagingUtilities.GetRandomString(length, MessagingUtilities.Digits); + default: + throw new ArgumentOutOfRangeException("format"); } } @@ -145,7 +180,9 @@ namespace DotNetOpenAuth.OAuth { /// <exception cref="ProtocolException">Thrown if an unexpected OAuth message is attached to the incoming request.</exception> public UnauthorizedTokenRequest ReadTokenRequest(HttpRequestInfo request) { UnauthorizedTokenRequest message; - this.Channel.TryReadFromRequest(request, out message); + if (this.Channel.TryReadFromRequest(request, out message)) { + ErrorUtilities.VerifyProtocol(message.Version >= Protocol.Lookup(this.SecuritySettings.MinimumRequiredOAuthVersion).Version, OAuthStrings.MinimumConsumerVersionRequirementNotMet, this.SecuritySettings.MinimumRequiredOAuthVersion, message.Version); + } return message; } @@ -156,9 +193,7 @@ namespace DotNetOpenAuth.OAuth { /// <param name="request">The token request message the Consumer sent that the Service Provider is now responding to.</param> /// <returns>The response message to send using the <see cref="Channel"/>, after optionally adding extra data to it.</returns> public UnauthorizedTokenResponse PrepareUnauthorizedTokenMessage(UnauthorizedTokenRequest request) { - if (request == null) { - throw new ArgumentNullException("request"); - } + ErrorUtilities.VerifyArgumentNotNull(request, "request"); string token = this.TokenGenerator.GenerateRequestToken(request.ConsumerKey); string secret = this.TokenGenerator.GenerateSecret(); @@ -207,11 +242,27 @@ namespace DotNetOpenAuth.OAuth { Contract.Requires(request != null); ErrorUtilities.VerifyArgumentNotNull(request, "request"); - if (request.Callback != null) { - return this.PrepareAuthorizationResponse(request, request.Callback); + // It is very important for us to ignore the oauth_callback argument in the + // UserAuthorizationRequest if the Consumer is a 1.0a consumer or else we + // open up a security exploit. + IServiceProviderRequestToken token = this.TokenManager.GetRequestToken(request.RequestToken); + Uri callback; + if (request.Version >= Protocol.V10a.Version) { + // In OAuth 1.0a, we'll prefer the token-specific callback to the pre-registered one. + if (token.Callback != null) { + callback = token.Callback; + } else { + IConsumerDescription consumer = this.TokenManager.GetConsumer(token.ConsumerKey); + callback = consumer.Callback; + } } else { - return null; + // In OAuth 1.0, we'll prefer the pre-registered callback over the token-specific one + // since 1.0 has a security weakness for user-modified callback URIs. + IConsumerDescription consumer = this.TokenManager.GetConsumer(token.ConsumerKey); + callback = consumer.Callback ?? request.Callback; } + + return callback != null ? this.PrepareAuthorizationResponse(request, callback) : null; } /// <summary> @@ -220,7 +271,7 @@ namespace DotNetOpenAuth.OAuth { /// </summary> /// <param name="request">The Consumer's original authorization request.</param> /// <param name="callback">The callback URI the consumer has previously registered - /// with this service provider.</param> + /// with this service provider or that came in the <see cref="UnauthorizedTokenRequest"/>.</param> /// <returns> /// The message to send to the Consumer using <see cref="Channel"/>. /// </returns> @@ -231,9 +282,14 @@ namespace DotNetOpenAuth.OAuth { ErrorUtilities.VerifyArgumentNotNull(request, "request"); ErrorUtilities.VerifyArgumentNotNull(callback, "callback"); - var authorization = new UserAuthorizationResponse(request.Callback) { + var authorization = new UserAuthorizationResponse(callback, request.Version) { RequestToken = request.RequestToken, }; + + if (authorization.Version >= Protocol.V10a.Version) { + authorization.VerificationCode = CreateVerificationCode(VerificationCodeFormat.IncludedInCallback, VerifierCodeLength); + } + return authorization; } @@ -267,17 +323,10 @@ namespace DotNetOpenAuth.OAuth { /// <param name="request">The Consumer's message requesting an access token.</param> /// <returns>The HTTP response to actually send to the Consumer.</returns> public AuthorizedTokenResponse PrepareAccessTokenMessage(AuthorizedTokenRequest request) { - if (request == null) { - throw new ArgumentNullException("request"); - } + Contract.Requires(request != null); + ErrorUtilities.VerifyArgumentNotNull(request, "request"); - if (!this.TokenManager.IsRequestTokenAuthorized(request.RequestToken)) { - throw new ProtocolException( - string.Format( - CultureInfo.CurrentCulture, - OAuthStrings.AccessTokenNotAuthorized, - request.RequestToken)); - } + ErrorUtilities.VerifyProtocol(this.TokenManager.IsRequestTokenAuthorized(request.RequestToken), OAuthStrings.AccessTokenNotAuthorized, request.RequestToken); string accessToken = this.TokenGenerator.GenerateAccessToken(request.ConsumerKey); string tokenSecret = this.TokenGenerator.GenerateSecret(); @@ -369,18 +418,5 @@ namespace DotNetOpenAuth.OAuth { } #endregion - - /// <summary> - /// Hooks the channel in order to perform some operations on some outgoing messages. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="DotNetOpenAuth.Messaging.ChannelEventArgs"/> instance containing the event data.</param> - private void OAuthChannel_Sending(object sender, ChannelEventArgs e) { - // Hook to store the token and secret on its way down to the Consumer. - var grantRequestTokenResponse = e.Message as UnauthorizedTokenResponse; - if (grantRequestTokenResponse != null) { - this.TokenManager.StoreNewRequestToken(grantRequestTokenResponse.RequestMessage, grantRequestTokenResponse); - } - } } } diff --git a/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs b/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs index 4636829..9014762 100644 --- a/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs +++ b/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs @@ -26,9 +26,15 @@ namespace DotNetOpenAuth.OAuth { /// Initializes a new instance of the <see cref="ServiceProviderDescription"/> class. /// </summary> public ServiceProviderDescription() { + this.ProtocolVersion = Protocol.Default.ProtocolVersion; } /// <summary> + /// Gets or sets the OAuth version supported by the Service Provider. + /// </summary> + public ProtocolVersion ProtocolVersion { get; set; } + + /// <summary> /// Gets or sets the URL used to obtain an unauthorized Request Token, /// described in Section 6.1 (Obtaining an Unauthorized Request Token). /// </summary> @@ -43,7 +49,7 @@ namespace DotNetOpenAuth.OAuth { } set { - if (value != null && UriUtil.QueryStringContainPrefixedParameters(value.Location, OAuth.Protocol.V10.ParameterPrefix)) { + if (value != null && UriUtil.QueryStringContainPrefixedParameters(value.Location, OAuth.Protocol.ParameterPrefix)) { throw new ArgumentException(OAuthStrings.RequestUrlMustNotHaveOAuthParameters); } @@ -77,6 +83,13 @@ namespace DotNetOpenAuth.OAuth { public ITamperProtectionChannelBindingElement[] TamperProtectionElements { get; set; } /// <summary> + /// Gets the OAuth version supported by the Service Provider. + /// </summary> + internal Version Version { + get { return Protocol.Lookup(this.ProtocolVersion).Version; } + } + + /// <summary> /// Creates a signing element that includes all the signing elements this service provider supports. /// </summary> /// <returns>The created signing element.</returns> diff --git a/src/DotNetOpenAuth/OAuth/ServiceProviderSecuritySettings.cs b/src/DotNetOpenAuth/OAuth/ServiceProviderSecuritySettings.cs new file mode 100644 index 0000000..094f54c --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ServiceProviderSecuritySettings.cs @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProviderSecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + /// <summary> + /// Security settings that are applicable to service providers. + /// </summary> + public class ServiceProviderSecuritySettings : SecuritySettings { + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderSecuritySettings"/> class. + /// </summary> + internal ServiceProviderSecuritySettings() { + } + + /// <summary> + /// Gets or sets the minimum required version of OAuth that must be implemented by a Consumer. + /// </summary> + public ProtocolVersion MinimumRequiredOAuthVersion { get; set; } + } +} diff --git a/src/DotNetOpenAuth/OAuth/VerificationCodeFormat.cs b/src/DotNetOpenAuth/OAuth/VerificationCodeFormat.cs new file mode 100644 index 0000000..d25c988 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/VerificationCodeFormat.cs @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------- +// <copyright file="VerificationCodeFormat.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + /// <summary> + /// The different formats a user authorization verifier code can take + /// in order to be as secure as possible while being compatible with + /// the type of OAuth Consumer requesting access. + /// </summary> + /// <remarks> + /// Some Consumers may be set-top boxes, video games, mobile devies, etc. + /// with very limited character entry support and no ability to receive + /// a callback URI. OAuth 1.0a requires that these devices operators + /// must manually key in a verifier code, so in these cases it better + /// be possible to do so given the input options on that device. + /// </remarks> + public enum VerificationCodeFormat { + /// <summary> + /// The strongest verification code. + /// The best option for web consumers since a callback is usually an option. + /// </summary> + IncludedInCallback, + + /// <summary> + /// A combination of upper and lowercase letters and numbers may be used, + /// allowing a computer operator to easily read from the screen and key + /// in the verification code. + /// </summary> + /// <remarks> + /// Some letters and numbers will be skipped where they are visually similar + /// enough that they can be difficult to distinguish when displayed with most fonts. + /// </remarks> + AlphaNumericNoLookAlikes, + + /// <summary> + /// Only uppercase letters will be used in the verification code. + /// Verification codes are case-sensitive, so consumers with fixed + /// keyboards with only one character case option may require this option. + /// </summary> + AlphaUpper, + + /// <summary> + /// Only lowercase letters will be used in the verification code. + /// Verification codes are case-sensitive, so consumers with fixed + /// keyboards with only one character case option may require this option. + /// </summary> + AlphaLower, + + /// <summary> + /// Only the numbers 0-9 will be used in the verification code. + /// Must useful for consumers running on mobile phone devices. + /// </summary> + Numeric, + } +} diff --git a/src/DotNetOpenAuth/OAuth/WebConsumer.cs b/src/DotNetOpenAuth/OAuth/WebConsumer.cs index bbf6115..11f3da8 100644 --- a/src/DotNetOpenAuth/OAuth/WebConsumer.cs +++ b/src/DotNetOpenAuth/OAuth/WebConsumer.cs @@ -39,7 +39,7 @@ namespace DotNetOpenAuth.OAuth { /// Requires HttpContext.Current. /// </remarks> public UserAuthorizationRequest PrepareRequestUserAuthorization() { - Uri callback = this.Channel.GetRequestFromContext().UrlBeforeRewriting.StripQueryArgumentsWithPrefix(Protocol.Default.ParameterPrefix); + Uri callback = this.Channel.GetRequestFromContext().UrlBeforeRewriting.StripQueryArgumentsWithPrefix(Protocol.ParameterPrefix); return this.PrepareRequestUserAuthorization(callback, null, null); } @@ -79,7 +79,8 @@ namespace DotNetOpenAuth.OAuth { UserAuthorizationResponse authorizationMessage; if (this.Channel.TryReadFromRequest<UserAuthorizationResponse>(request, out authorizationMessage)) { string requestToken = authorizationMessage.RequestToken; - return this.ProcessUserAuthorization(requestToken); + string verifier = authorizationMessage.VerificationCode; + return this.ProcessUserAuthorization(requestToken, verifier); } else { return null; } diff --git a/src/DotNetOpenAuth/OpenId/Protocol.cs b/src/DotNetOpenAuth/OpenId/Protocol.cs index b9f2cca..7b8a2f1 100644 --- a/src/DotNetOpenAuth/OpenId/Protocol.cs +++ b/src/DotNetOpenAuth/OpenId/Protocol.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OpenId { using DotNetOpenAuth.Messaging; using System.Globalization; using System.Diagnostics.CodeAnalysis; + using System.Diagnostics; /// <summary> /// An enumeration of the OpenID protocol versions supported by this library. @@ -34,6 +35,7 @@ namespace DotNetOpenAuth.OpenId { /// Tracks the several versions of OpenID this library supports and the unique /// constants to each version used in the protocol. /// </summary> + [DebuggerDisplay("OpenID {Version}")] internal class Protocol { /// <summary> /// The value of the openid.ns parameter in the OpenID 2.0 specification. diff --git a/src/DotNetOpenAuth/Yadis/HtmlParser.cs b/src/DotNetOpenAuth/Yadis/HtmlParser.cs index a6b64c1..406cb4b 100644 --- a/src/DotNetOpenAuth/Yadis/HtmlParser.cs +++ b/src/DotNetOpenAuth/Yadis/HtmlParser.cs @@ -98,8 +98,8 @@ namespace DotNetOpenAuth.Yadis { /// <param name="attribute">The attribute.</param> /// <returns>A filtered sequence of attributes.</returns> internal static IEnumerable<T> WithAttribute<T>(this IEnumerable<T> sequence, string attribute) where T : HtmlControl { - Contract.Requires<ArgumentNullException>(sequence != null); - Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(attribute)); + Contract.Requires(sequence != null); + Contract.Requires(!String.IsNullOrEmpty(attribute)); return sequence.Where(tag => tag.Attributes[attribute] != null); } |