diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2009-06-25 19:51:02 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2009-06-25 19:51:02 -0700 |
commit | 3b2fe4819cf449d1ab88178d055239c64b3cfd5e (patch) | |
tree | e191391cc71af22a854a06fee0079dbb74405752 /src/DotNetOpenAuth/OAuth/ChannelElements | |
parent | 97e3fc44a6911289baf3435febc0b003e56ad4e8 (diff) | |
parent | f3ce247dfaa965011c7f8417d00e0fa3dfad4a35 (diff) | |
download | DotNetOpenAuth-3b2fe4819cf449d1ab88178d055239c64b3cfd5e.zip DotNetOpenAuth-3b2fe4819cf449d1ab88178d055239c64b3cfd5e.tar.gz DotNetOpenAuth-3b2fe4819cf449d1ab88178d055239c64b3cfd5e.tar.bz2 |
Merge branch 'master' into contracts
Conflicts:
src/DotNetOpenAuth.vsmdi
src/DotNetOpenAuth/Configuration/TypeConfigurationCollection.cs
src/DotNetOpenAuth/Configuration/TypeConfigurationElement.cs
src/DotNetOpenAuth/DotNetOpenAuth.csproj
src/DotNetOpenAuth/Messaging/HttpRequestInfo.cs
src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs
src/DotNetOpenAuth/Messaging/Reflection/ValueMapping.cs
src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs
src/DotNetOpenAuth/OAuth/ConsumerBase.cs
src/DotNetOpenAuth/OAuth/Messages/MessageBase.cs
src/DotNetOpenAuth/OAuth/Messages/UnauthorizedTokenResponse.cs
src/DotNetOpenAuth/OAuth/ServiceProvider.cs
src/DotNetOpenAuth/OpenId/Protocol.cs
src/DotNetOpenAuth/OpenId/Provider/AutoResponsiveRequest.cs
src/DotNetOpenAuth/OpenId/Provider/HostProcessedRequest.cs
src/DotNetOpenAuth/OpenId/Provider/IHostProcessedRequest.cs
src/DotNetOpenAuth/OpenId/Provider/Request.cs
src/DotNetOpenAuth/OpenId/RelyingParty/OpenIdRelyingParty.cs
src/DotNetOpenAuth/OpenId/RelyingParty/ServiceEndpoint.cs
Diffstat (limited to 'src/DotNetOpenAuth/OAuth/ChannelElements')
13 files changed, 716 insertions, 83 deletions
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/IServiceProviderAccessToken.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderAccessToken.cs new file mode 100644 index 0000000..329ac4d --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderAccessToken.cs @@ -0,0 +1,45 @@ +//----------------------------------------------------------------------- +// <copyright file="IServiceProviderAccessToken.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; + + /// <summary> + /// A description of an access token and its metadata as required by a Service Provider. + /// </summary> + public interface IServiceProviderAccessToken { + /// <summary> + /// Gets the token itself. + /// </summary> + string Token { get; } + + /// <summary> + /// Gets the expiration date (local time) for the access token. + /// </summary> + /// <value>The expiration date, or <c>null</c> if there is no expiration date.</value> + DateTime? ExpirationDate { get; } + + /// <summary> + /// Gets the username of the principal that will be impersonated by this access token. + /// </summary> + /// <value> + /// The name of the user who authorized the OAuth request token originally. + /// </value> + string Username { get; } + + /// <summary> + /// Gets the roles that the OAuth principal should belong to. + /// </summary> + /// <value> + /// The roles that the user belongs to, or a subset of these according to the rights + /// granted when the user authorized the request token. + /// </value> + string[] Roles { get; } + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs new file mode 100644 index 0000000..6dfa416 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderRequestToken.cs @@ -0,0 +1,52 @@ +//----------------------------------------------------------------------- +// <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 the (local) date that this request token was first created on. + /// </summary> + DateTime CreatedOn { 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..02ebffb 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/IServiceProviderTokenManager.cs @@ -16,12 +16,39 @@ 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> + /// <remarks> + /// It is acceptable for implementations to find the token, see that it has expired, + /// delete it from the database and then throw <see cref="KeyNotFoundException"/>, + /// or alternatively it can return the expired token anyway and the OAuth channel will + /// log and throw the appropriate error. + /// </remarks> + IServiceProviderRequestToken GetRequestToken(string token); + + /// <summary> + /// Gets details on the named access token. + /// </summary> + /// <param name="token">The access token.</param> + /// <returns>A description of the token. Never null.</returns> + /// <exception cref="KeyNotFoundException">Thrown if the token cannot be found.</exception> + /// <remarks> + /// It is acceptable for implementations to find the token, see that it has expired, + /// delete it from the database and then throw <see cref="KeyNotFoundException"/>, + /// or alternatively it can return the expired token anyway and the OAuth channel will + /// log and throw the appropriate error. + /// </remarks> + IServiceProviderAccessToken GetAccessToken(string token); } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index cd34f6f..a36287f 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)) { Contract.Requires<ArgumentNullException>(tokenManager != null); this.TokenManager = tokenManager; @@ -120,7 +120,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. @@ -140,8 +140,10 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { } // Scrape the entity - foreach (string key in request.Form) { - fields.Add(key, request.Form[key]); + if (string.Equals(request.Headers[HttpRequestHeader.ContentType], HttpFormUrlEncoded, StringComparison.Ordinal)) { + foreach (string key in request.Form) { + fields.Add(key, request.Form[key]); + } } // Scrape the query string @@ -155,7 +157,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { // Add receiving HTTP transport information required for signature generation. var signedMessage = message as ITamperResistantOAuthMessage; if (signedMessage != null) { - signedMessage.Recipient = request.Url; + signedMessage.Recipient = request.UrlBeforeRewriting; signedMessage.HttpMethod = request.HttpMethod; } @@ -230,6 +232,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> @@ -295,7 +320,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); @@ -354,7 +379,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 5d76afc..327b923 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthConsumerMessageFactory.cs @@ -42,7 +42,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) { @@ -87,7 +88,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/OAuthIdentity.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthIdentity.cs new file mode 100644 index 0000000..0de2c15 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthIdentity.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthIdentity.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using System.Runtime.InteropServices; + using System.Security.Principal; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Represents an OAuth consumer that is impersonating a known user on the system. + /// </summary> + [Serializable] + [ComVisible(true)] + public class OAuthIdentity : IIdentity { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthIdentity"/> class. + /// </summary> + /// <param name="username">The username.</param> + internal OAuthIdentity(string username) { + Contract.Requires(!String.IsNullOrEmpty(username)); + ErrorUtilities.VerifyNonZeroLength(username, "username"); + this.Name = username; + } + + #region IIdentity Members + + /// <summary> + /// Gets the type of authentication used. + /// </summary> + /// <value>The constant "OAuth"</value> + /// <returns> + /// The type of authentication used to identify the user. + /// </returns> + public string AuthenticationType { + get { return "OAuth"; } + } + + /// <summary> + /// Gets a value indicating whether the user has been authenticated. + /// </summary> + /// <value>The value <c>true</c></value> + /// <returns>true if the user was authenticated; otherwise, false. + /// </returns> + public bool IsAuthenticated { + get { return true; } + } + + /// <summary> + /// Gets the name of the user who authorized the OAuth token the consumer is using for authorization. + /// </summary> + /// <returns> + /// The name of the user on whose behalf the code is running. + /// </returns> + public string Name { get; private set; } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthPrincipal.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthPrincipal.cs new file mode 100644 index 0000000..689c388 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthPrincipal.cs @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthPrincipal.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.Runtime.InteropServices; + using System.Security.Principal; + + /// <summary> + /// Represents an OAuth consumer that is impersonating a known user on the system. + /// </summary> + [Serializable] + [ComVisible(true)] + public class OAuthPrincipal : IPrincipal { + /// <summary> + /// The roles this user belongs to. + /// </summary> + private ICollection<string> roles; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthPrincipal"/> class. + /// </summary> + /// <param name="token">The access token.</param> + internal OAuthPrincipal(IServiceProviderAccessToken token) + : this(token.Username, token.Roles) { + Contract.Requires(token != null); + + this.AccessToken = token.Token; + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthPrincipal"/> class. + /// </summary> + /// <param name="identity">The identity.</param> + /// <param name="roles">The roles this user belongs to.</param> + internal OAuthPrincipal(OAuthIdentity identity, string[] roles) { + this.Identity = identity; + this.roles = roles; + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthPrincipal"/> class. + /// </summary> + /// <param name="username">The username.</param> + /// <param name="roles">The roles this user belongs to.</param> + internal OAuthPrincipal(string username, string[] roles) + : this(new OAuthIdentity(username), roles) { + } + + /// <summary> + /// Gets the access token used to create this principal. + /// </summary> + /// <value>A non-empty string.</value> + public string AccessToken { get; private set; } + + #region IPrincipal Members + + /// <summary> + /// Gets the identity of the current principal. + /// </summary> + /// <value></value> + /// <returns> + /// The <see cref="T:System.Security.Principal.IIdentity"/> object associated with the current principal. + /// </returns> + public IIdentity Identity { get; private set; } + + /// <summary> + /// Determines whether the current principal belongs to the specified role. + /// </summary> + /// <param name="role">The name of the role for which to check membership.</param> + /// <returns> + /// true if the current principal is a member of the specified role; otherwise, false. + /// </returns> + /// <remarks> + /// The role membership check uses <see cref="StringComparer.OrdinalIgnoreCase"/>. + /// </remarks> + public bool IsInRole(string role) { + return this.roles.Contains(role, StringComparer.OrdinalIgnoreCase); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs index 767a0b9..429615a 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthServiceProviderMessageFactory.cs @@ -52,29 +52,51 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </remarks> public virtual IDirectedProtocolMessage GetNewRequestMessage(MessageReceivingEndpoint recipient, IDictionary<string, string> 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..ce7bb98 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/TokenHandlingBindingElement.cs @@ -0,0 +1,190 @@ +//----------------------------------------------------------------------- +// <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.Configuration; + 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) { + if (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; + } + + this.VerifyThrowTokenTimeToLive(authorizedTokenRequest); + } + + var userAuthorizationRequest = message as UserAuthorizationRequest; + if (userAuthorizationRequest != null) { + this.VerifyThrowTokenTimeToLive(userAuthorizationRequest); + } + + var accessResourceRequest = message as AccessProtectedResourceRequest; + if (accessResourceRequest != null) { + this.VerifyThrowTokenNotExpired(accessResourceRequest); + } + + return null; + } + + #endregion + + /// <summary> + /// Ensures that access tokens have not yet expired. + /// </summary> + /// <param name="message">The incoming message carrying the access token.</param> + private void VerifyThrowTokenNotExpired(AccessProtectedResourceRequest message) { + ErrorUtilities.VerifyArgumentNotNull(message, "message"); + + try { + IServiceProviderAccessToken token = this.tokenManager.GetAccessToken(message.AccessToken); + if (token.ExpirationDate.HasValue && DateTime.Now >= token.ExpirationDate.Value.ToLocalTime()) { + Logger.OAuth.ErrorFormat( + "OAuth access token {0} rejected because it expired at {1}, and it is now {2}.", + token.Token, + token.ExpirationDate.Value, + DateTime.Now); + ErrorUtilities.ThrowProtocol(OAuthStrings.TokenNotFound); + } + } catch (KeyNotFoundException ex) { + throw ErrorUtilities.Wrap(ex, OAuthStrings.TokenNotFound); + } + } + + /// <summary> + /// Ensures that short-lived request tokens included in incoming messages have not expired. + /// </summary> + /// <param name="message">The incoming message.</param> + /// <exception cref="ProtocolException">Thrown when the token in the message has expired.</exception> + private void VerifyThrowTokenTimeToLive(ITokenContainingMessage message) { + ErrorUtilities.VerifyInternal(!(message is AccessProtectedResourceRequest), "We shouldn't be verifying TTL on access tokens."); + if (message == null) { + return; + } + + try { + IServiceProviderRequestToken token = this.tokenManager.GetRequestToken(message.Token); + TimeSpan ttl = DotNetOpenAuthSection.Configuration.OAuth.ServiceProvider.SecuritySettings.MaximumRequestTokenTimeToLive; + if (DateTime.Now >= token.CreatedOn.ToLocalTime() + ttl) { + Logger.OAuth.ErrorFormat( + "OAuth request token {0} rejected because it was originally issued at {1}, expired at {2}, and it is now {3}.", + token.Token, + token.CreatedOn, + token.CreatedOn + ttl, + DateTime.Now); + ErrorUtilities.ThrowProtocol(OAuthStrings.TokenNotFound); + } + } catch (KeyNotFoundException ex) { + throw ErrorUtilities.Wrap(ex, OAuthStrings.TokenNotFound); + } + } + } +} 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 + } +} |