diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-01-29 14:32:45 -0800 |
commit | 5fec515095ee10b522f414a03e78f282aaf520dc (patch) | |
tree | 204c75486639c23cdda2ef38b34d7e5050a1a2e3 /src/DotNetOpenAuth.OAuth/OAuth | |
parent | f1a4155398635a4fd9f485eec817152627682704 (diff) | |
parent | 8f4165ee515728aca3faaa26e8354a40612e85e4 (diff) | |
download | DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.zip DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.gz DotNetOpenAuth-5fec515095ee10b522f414a03e78f282aaf520dc.tar.bz2 |
Merge branch 'splitDlls'.
DNOA now builds and (in some cases) ships as many distinct assemblies.
Diffstat (limited to 'src/DotNetOpenAuth.OAuth/OAuth')
35 files changed, 3524 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs new file mode 100644 index 0000000..5828428 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/HmacSha1SigningBindingElement.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// <copyright file="HmacSha1SigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + public class HmacSha1SigningBindingElement : SigningBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="HmacSha1SigningBindingElement"/> class + /// </summary> + public HmacSha1SigningBindingElement() + : base("HMAC-SHA1") { + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message per OAuth 1.0 section 9.2. + /// </remarks> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + string key = GetConsumerAndTokenSecretString(message); + using (HashAlgorithm hasher = new HMACSHA1(Encoding.ASCII.GetBytes(key))) { + string baseString = ConstructSignatureBaseString(message, this.Channel.MessageDescriptions.GetAccessor(message)); + byte[] digest = hasher.ComputeHash(Encoding.ASCII.GetBytes(baseString)); + return Convert.ToBase64String(digest); + } + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + protected override ITamperProtectionChannelBindingElement Clone() { + return new HmacSha1SigningBindingElement(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs new file mode 100644 index 0000000..dd28e71 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="ICombinedOpenIdProviderTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using DotNetOpenAuth.OpenId; + + /// <summary> + /// An interface that providers that play a dual role as OpenID Provider + /// and OAuth Service Provider should implement on their token manager classes. + /// </summary> + /// <remarks> + /// This interface should be implemented by the same class that implements + /// <see cref="ITokenManager"/> in order to enable the OpenID+OAuth extension. + /// </remarks> + public interface ICombinedOpenIdProviderTokenManager : IOpenIdOAuthTokenManager, ITokenManager { + /// <summary> + /// Gets the OAuth consumer key for a given OpenID relying party realm. + /// </summary> + /// <param name="realm">The relying party's OpenID realm.</param> + /// <returns>The OAuth consumer key for a given OpenID realm.</returns> + /// <para>This is a security-critical function. Since OpenID requests + /// and OAuth extensions for those requests can be formulated by ANYONE + /// (no signing is required by the relying party), and since the response to + /// the authentication will include access the user is granted to the + /// relying party who CLAIMS to be from some realm, it is of paramount + /// importance that the realm is recognized as belonging to the consumer + /// key by the host service provider in order to protect against phishers.</para> + string GetConsumerKey(Realm realm); + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs new file mode 100644 index 0000000..b3ee320 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdOAuthTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + + /// <summary> + /// Additional methods an <see cref="ITokenManager"/> implementing class + /// may implement to support the OpenID+OAuth extension. + /// </summary> + public interface IOpenIdOAuthTokenManager { + /// <summary> + /// Stores a new request token obtained over an OpenID request. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param> + /// <remarks> + /// <para>The token secret is the empty string.</para> + /// <para>Tokens stored by this method should be short-lived to mitigate + /// possible security threats. Their lifetime should be sufficient for the + /// relying party to receive the positive authentication assertion and immediately + /// send a follow-up request for the access token.</para> + /// </remarks> + void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization); + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs new file mode 100644 index 0000000..a95001d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITamperResistantOAuthMessage.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="ITamperResistantOAuthMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// An interface that OAuth messages implement to support signing. + /// </summary> + public interface ITamperResistantOAuthMessage : IDirectedProtocolMessage, ITamperResistantProtocolMessage, IMessageOriginalPayload { + /// <summary> + /// Gets or sets the method used to sign the message. + /// </summary> + string SignatureMethod { get; set; } + + /// <summary> + /// Gets or sets the Token Secret used to sign the message. + /// </summary> + string TokenSecret { get; set; } + + /// <summary> + /// Gets or sets the Consumer key. + /// </summary> + string ConsumerKey { get; set; } + + /// <summary> + /// Gets or sets the Consumer Secret used to sign the message. + /// </summary> + string ConsumerSecret { get; set; } + + /// <summary> + /// Gets or sets the HTTP method that will be used to transmit the message. + /// </summary> + string HttpMethod { get; set; } + + /// <summary> + /// Gets or sets the URL of the intended receiver of this message. + /// </summary> + new Uri Recipient { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs new file mode 100644 index 0000000..7d68b63 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/ITokenManager.cs @@ -0,0 +1,169 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenManager.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.OAuth.Messages; + + /// <summary> + /// An interface OAuth hosts must implement for persistent storage + /// and recall of tokens and secrets for an individual OAuth consumer + /// or service provider. + /// </summary> + [ContractClass(typeof(ITokenManagerContract))] + public interface ITokenManager { + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns>The secret associated with the given token.</returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + string GetTokenSecret(string token); + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response); + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until WebConsumer.ProcessUserAuthorization or + /// DesktopConsumer.ProcessUserAuthorization return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret); + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns>Request or Access token, or invalid if the token is not recognized.</returns> + TokenType GetTokenType(string token); + } + + /// <summary> + /// The code contract class for the <see cref="ITokenManager"/> interface. + /// </summary> + [ContractClassFor(typeof(ITokenManager))] + internal abstract class ITokenManagerContract : ITokenManager { + /// <summary> + /// Prevents a default instance of the <see cref="ITokenManagerContract"/> class from being created. + /// </summary> + private ITokenManagerContract() { + } + + #region ITokenManager Members + + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns> + /// The secret associated with the given token. + /// </returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + string ITokenManager.GetTokenSecret(string token) { + Requires.NotNullOrEmpty(token, "token"); + Contract.Ensures(Contract.Result<string>() != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ITokenManager.ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + void ITokenManager.StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) { + Requires.NotNull(request, "request"); + Requires.NotNull(response, "response"); + throw new NotImplementedException(); + } + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="ITokenManager.StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until WebConsumer.ProcessUserAuthorization or + /// DesktopConsumer.ProcessUserAuthorization return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + void ITokenManager.ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) { + Requires.NotNullOrEmpty(consumerKey, "consumerKey"); + Requires.NotNullOrEmpty(requestToken, "requestToken"); + Requires.NotNullOrEmpty(accessToken, "accessToken"); + Requires.NotNull(accessTokenSecret, "accessTokenSecret"); + throw new NotImplementedException(); + } + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns> + /// Request or Access token, or invalid if the token is not recognized. + /// </returns> + TokenType ITokenManager.GetTokenType(string token) { + Requires.NotNullOrEmpty(token, "token"); + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs new file mode 100644 index 0000000..32b57d0 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -0,0 +1,356 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthChannel.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; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// An OAuth-specific implementation of the <see cref="Channel"/> class. + /// </summary> + internal abstract class OAuthChannel : Channel { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthChannel"/> class. + /// </summary> + /// <param name="signingBindingElement">The binding element to use for signing.</param> + /// <param name="store">The web application store to use for nonces.</param> + /// <param name="tokenManager">The ITokenManager instance to use.</param> + /// <param name="securitySettings">The security settings.</param> + /// <param name="messageTypeProvider">An injected message type provider instance. + /// Except for mock testing, this should always be one of + /// OAuthConsumerMessageFactory or OAuthServiceProviderMessageFactory.</param> + /// <param name="bindingElements">The binding elements.</param> + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires<System.ArgumentNullException>(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")] + protected OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, SecuritySettings securitySettings, IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements) + : base(messageTypeProvider, bindingElements) { + Requires.NotNull(tokenManager, "tokenManager"); + Requires.NotNull(securitySettings, "securitySettings"); + Requires.NotNull(signingBindingElement, "signingBindingElement"); + Requires.True(signingBindingElement.SignatureCallback == null, "signingBindingElement", OAuthStrings.SigningElementAlreadyAssociatedWithChannel); + Requires.NotNull(bindingElements, "bindingElements"); + + this.TokenManager = tokenManager; + signingBindingElement.SignatureCallback = this.SignatureCallback; + } + + /// <summary> + /// Gets or sets the Consumer web application path. + /// </summary> + internal Uri Realm { get; set; } + + /// <summary> + /// Gets the token manager being used. + /// </summary> + protected internal ITokenManager TokenManager { get; private set; } + + /// <summary> + /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1. + /// </summary> + /// <param name="message">The message with data to encode.</param> + /// <returns>A dictionary of name-value pairs with their strings encoded.</returns> + internal static IDictionary<string, string> GetUriEscapedParameters(IEnumerable<KeyValuePair<string, string>> message) { + var encodedDictionary = new Dictionary<string, string>(); + UriEscapeParameters(message, encodedDictionary); + return encodedDictionary; + } + + /// <summary> + /// Initializes a web request for sending by attaching a message to it. + /// Use this method to prepare a protected resource request that you do NOT + /// expect an OAuth message response to. + /// </summary> + /// <param name="request">The message to attach.</param> + /// <returns>The initialized web request.</returns> + internal HttpWebRequest InitializeRequest(IDirectedProtocolMessage request) { + Requires.NotNull(request, "request"); + + ProcessOutgoingMessage(request); + return this.CreateHttpRequest(request); + } + + /// <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> + /// <param name="securitySettings">The security settings.</param> + /// <returns> + /// An array of binding elements used to initialize the channel. + /// </returns> + protected static List<IChannelBindingElement> InitializeBindingElements(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager, SecuritySettings securitySettings) { + Contract.Requires(securitySettings != null); + + var bindingElements = new List<IChannelBindingElement> { + new OAuthHttpMethodBindingElement(), + signingBindingElement, + new StandardExpirationBindingElement(), + new StandardReplayProtectionBindingElement(store), + }; + + return bindingElements; + } + + /// <summary> + /// Searches an incoming HTTP request for data that could be used to assemble + /// a protocol request message. + /// </summary> + /// <param name="request">The HTTP request to search.</param> + /// <returns>The deserialized message, if one is found. Null otherwise.</returns> + protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) { + // First search the Authorization header. + string authorization = request.Headers[HttpRequestHeader.Authorization]; + var fields = MessagingUtilities.ParseAuthorizationHeader(Protocol.AuthorizationHeaderScheme, authorization).ToDictionary(); + fields.Remove("realm"); // ignore the realm parameter, since we don't use it, and it must be omitted from signature base string. + + // Scrape the entity + if (!string.IsNullOrEmpty(request.Headers[HttpRequestHeader.ContentType])) { + var contentType = new ContentType(request.Headers[HttpRequestHeader.ContentType]); + if (string.Equals(contentType.MediaType, HttpFormUrlEncoded, StringComparison.Ordinal)) { + foreach (string key in request.Form) { + if (key != null) { + fields.Add(key, request.Form[key]); + } else { + Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.Form[key]); + } + } + } + } + + // Scrape the query string + foreach (string key in request.QueryStringBeforeRewriting) { + if (key != null) { + fields.Add(key, request.QueryStringBeforeRewriting[key]); + } else { + Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", request.QueryStringBeforeRewriting[key]); + } + } + + MessageReceivingEndpoint recipient; + try { + recipient = request.GetRecipient(); + } catch (ArgumentException ex) { + Logger.OAuth.WarnFormat("Unrecognized HTTP request: " + ex.ToString()); + return null; + } + + // Deserialize the message using all the data we've collected. + var message = (IDirectedProtocolMessage)this.Receive(fields, recipient); + + // Add receiving HTTP transport information required for signature generation. + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null) { + signedMessage.Recipient = request.UrlBeforeRewriting; + signedMessage.HttpMethod = request.HttpMethod; + } + + return message; + } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + string body = response.GetResponseReader().ReadToEnd(); + return HttpUtility.ParseQueryString(body).ToDictionary(); + } + + /// <summary> + /// Prepares an HTTP request that carries a given message. + /// </summary> + /// <param name="request">The message to send.</param> + /// <returns> + /// The <see cref="HttpRequest"/> prepared to send the request. + /// </returns> + protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { + HttpWebRequest httpRequest; + + HttpDeliveryMethods transmissionMethod = request.HttpMethods; + if ((transmissionMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) { + httpRequest = this.InitializeRequestAsAuthHeader(request); + } else if ((transmissionMethod & HttpDeliveryMethods.PostRequest) != 0) { + var requestMessageWithBinaryData = request as IMessageWithBinaryData; + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || !requestMessageWithBinaryData.SendAsMultipart, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader); + httpRequest = this.InitializeRequestAsPost(request); + } else if ((transmissionMethod & HttpDeliveryMethods.GetRequest) != 0) { + httpRequest = InitializeRequestAsGet(request); + } else if ((transmissionMethod & HttpDeliveryMethods.HeadRequest) != 0) { + httpRequest = InitializeRequestAsHead(request); + } else if ((transmissionMethod & HttpDeliveryMethods.PutRequest) != 0) { + httpRequest = this.InitializeRequestAsPut(request); + } else if ((transmissionMethod & HttpDeliveryMethods.DeleteRequest) != 0) { + httpRequest = InitializeRequestAsDelete(request); + } else { + throw new NotSupportedException(); + } + return httpRequest; + } + + /// <summary> + /// Queues a message for sending in the response stream where the fields + /// are sent in the response stream in querystring style. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns>The pending user agent redirect based message to be sent as an HttpResponse.</returns> + /// <remarks> + /// This method implements spec V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + var messageAccessor = this.MessageDescriptions.GetAccessor(response); + var fields = messageAccessor.Serialize(); + string responseBody = MessagingUtilities.CreateQueryString(fields); + + OutgoingWebResponse encodedResponse = new OutgoingWebResponse { + Body = responseBody, + OriginalMessage = response, + Status = HttpStatusCode.OK, + Headers = new System.Net.WebHeaderCollection(), + }; + + IHttpDirectResponse httpMessage = response as IHttpDirectResponse; + if (httpMessage != null) { + encodedResponse.Status = httpMessage.HttpStatusCode; + } + + return encodedResponse; + } + + /// <summary> + /// Gets the consumer secret for a given consumer key. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <returns>A consumer secret.</returns> + protected abstract string GetConsumerSecret(string consumerKey); + + /// <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> + /// <param name="destination">The dictionary to add the encoded pairs to.</param> + private static void UriEscapeParameters(IEnumerable<KeyValuePair<string, string>> source, IDictionary<string, string> destination) { + Requires.NotNull(source, "source"); + Requires.NotNull(destination, "destination"); + + foreach (var pair in source) { + var key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key); + var value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value); + destination.Add(key, value); + } + } + + /// <summary> + /// Gets the HTTP method to use for a message. + /// </summary> + /// <param name="message">The message.</param> + /// <returns>"POST", "GET" or some other similar http verb.</returns> + private static string GetHttpMethod(IDirectedProtocolMessage message) { + Requires.NotNull(message, "message"); + + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null) { + return signedMessage.HttpMethod; + } else { + return MessagingUtilities.GetHttpVerb(message.HttpMethods); + } + } + + /// <summary> + /// Prepares to send a request to the Service Provider via the Authorization header. + /// </summary> + /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param> + /// <returns>The web request ready to send.</returns> + /// <remarks> + /// <para>If the message has non-empty ExtraData in it, the request stream is sent to + /// the server automatically. If it is empty, the request stream must be sent by the caller.</para> + /// <para>This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4).</para> + /// </remarks> + private HttpWebRequest InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) { + var dictionary = this.MessageDescriptions.GetAccessor(requestMessage); + + // copy so as to not modify original + var fields = new Dictionary<string, string>(); + foreach (string key in dictionary.DeclaredKeys) { + fields.Add(key, dictionary[key]); + } + if (this.Realm != null) { + fields.Add("realm", this.Realm.AbsoluteUri); + } + + HttpWebRequest httpRequest; + UriBuilder recipientBuilder = new UriBuilder(requestMessage.Recipient); + bool hasEntity = HttpMethodHasEntity(GetHttpMethod(requestMessage)); + + if (!hasEntity) { + MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData); + } + httpRequest = (HttpWebRequest)WebRequest.Create(recipientBuilder.Uri); + httpRequest.Method = GetHttpMethod(requestMessage); + + httpRequest.Headers.Add(HttpRequestHeader.Authorization, MessagingUtilities.AssembleAuthorizationHeader(Protocol.AuthorizationHeaderScheme, fields)); + + if (hasEntity) { + // WARNING: We only set up the request stream for the caller if there is + // extra data. If there isn't any extra data, the caller must do this themselves. + var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData; + if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) { + // Include the binary data in the multipart entity, and any standard text extra message data. + // The standard declared message parts are included in the authorization header. + var multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData); + multiPartFields.AddRange(requestMessage.ExtraData.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value))); + this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields); + } else { + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); + if (requestMessage.ExtraData.Count > 0) { + this.SendParametersInEntity(httpRequest, requestMessage.ExtraData); + } else { + // We'll assume the content length is zero since the caller may not have + // anything. They're responsible to change it when the add the payload if they have one. + httpRequest.ContentLength = 0; + } + } + } + + return httpRequest; + } + + /// <summary> + /// Fills out the secrets in a message so that signing/verification can be performed. + /// </summary> + /// <param name="message">The message about to be signed or whose signature is about to be verified.</param> + private void SignatureCallback(ITamperResistantProtocolMessage message) { + var oauthMessage = message as ITamperResistantOAuthMessage; + try { + Logger.Channel.Debug("Applying secrets to message to prepare for signing or signature verification."); + oauthMessage.ConsumerSecret = this.GetConsumerSecret(oauthMessage.ConsumerKey); + + var tokenMessage = message as ITokenContainingMessage; + if (tokenMessage != null) { + oauthMessage.TokenSecret = this.TokenManager.GetTokenSecret(tokenMessage.Token); + } + } catch (KeyNotFoundException ex) { + throw new ProtocolException(OAuthStrings.ConsumerOrTokenSecretNotFound, ex); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs new file mode 100644 index 0000000..37fb80b --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthHttpMethodBindingElement.cs @@ -0,0 +1,76 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthHttpMethodBindingElement.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; + + /// <summary> + /// Sets the HTTP Method property on a signed message before the signing module gets to it. + /// </summary> + internal class OAuthHttpMethodBindingElement : IChannelBindingElement { + #region IChannelBindingElement Members + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + public MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + /// <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> + /// True if the <paramref name="message"/> applied to this binding element + /// and the operation was successful. False otherwise. + /// </returns> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var oauthMessage = message as ITamperResistantOAuthMessage; + + if (oauthMessage != null) { + HttpDeliveryMethods transmissionMethod = oauthMessage.HttpMethods; + try { + oauthMessage.HttpMethod = MessagingUtilities.GetHttpVerb(transmissionMethod); + return MessageProtections.None; + } catch (ArgumentException ex) { + Logger.OAuth.Error("Unrecognized HttpDeliveryMethods value.", ex); + return null; + } + } else { + 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> + /// True if the <paramref name="message"/> applied to this binding element + /// and the operation was successful. False if the operation did not apply to this message. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs new file mode 100644 index 0000000..22e5f20 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/PlaintextSigningBindingElement.cs @@ -0,0 +1,60 @@ +//----------------------------------------------------------------------- +// <copyright file="PlaintextSigningBindingElement.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.Bindings; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + public class PlaintextSigningBindingElement : SigningBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="PlaintextSigningBindingElement"/> class. + /// </summary> + public PlaintextSigningBindingElement() + : base("PLAINTEXT") { + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message according to OAuth 1.0 section 9.4.1. + /// </remarks> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + return GetConsumerAndTokenSecretString(message); + } + + /// <summary> + /// Checks whether this binding element applies to this message. + /// </summary> + /// <param name="message">The message that needs to be signed.</param> + /// <returns>True if this binding element can be used to sign the message. False otherwise.</returns> + protected override bool IsMessageApplicable(ITamperResistantOAuthMessage message) { + if (string.Equals(message.Recipient.Scheme, "https", StringComparison.OrdinalIgnoreCase)) { + return true; + } else { + Logger.Bindings.DebugFormat("The {0} element will not sign this message because the URI scheme is not https.", this.GetType().Name); + return false; + } + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + protected override ITamperProtectionChannelBindingElement Clone() { + return new PlaintextSigningBindingElement(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs new file mode 100644 index 0000000..83be094 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/RsaSha1SigningBindingElement.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------- +// <copyright file="RsaSha1SigningBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + public abstract class RsaSha1SigningBindingElement : SigningBindingElementBase { + /// <summary> + /// The name of the hash algorithm to use. + /// </summary> + protected const string HashAlgorithmName = "RSA-SHA1"; + + /// <summary> + /// Initializes a new instance of the <see cref="RsaSha1SigningBindingElement"/> class. + /// </summary> + protected RsaSha1SigningBindingElement() + : base(HashAlgorithmName) { + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs new file mode 100644 index 0000000..2c47453 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -0,0 +1,329 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// A binding element that signs outgoing messages and verifies the signature on incoming messages. + /// </summary> + [ContractClass(typeof(SigningBindingElementBaseContract))] + public abstract class SigningBindingElementBase : ITamperProtectionChannelBindingElement { + /// <summary> + /// The signature method this binding element uses. + /// </summary> + private string signatureMethod; + + /// <summary> + /// Initializes a new instance of the <see cref="SigningBindingElementBase"/> class. + /// </summary> + /// <param name="signatureMethod">The OAuth signature method that the binding element uses.</param> + internal SigningBindingElementBase(string signatureMethod) { + this.signatureMethod = signatureMethod; + } + + #region IChannelBindingElement Properties + + /// <summary> + /// Gets the message protection provided by this binding element. + /// </summary> + public MessageProtections Protection { + get { return MessageProtections.TamperProtection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { get; set; } + + #endregion + + #region ITamperProtectionChannelBindingElement members + + /// <summary> + /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a signed + /// message so that its signature can be correctly calculated for verification. + /// </summary> + public Action<ITamperResistantOAuthMessage> SignatureCallback { get; set; } + + /// <summary> + /// Creates a new object that is a copy of the current instance. + /// </summary> + /// <returns> + /// A new object that is a copy of this instance. + /// </returns> + ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() { + ITamperProtectionChannelBindingElement clone = this.Clone(); + clone.SignatureCallback = this.SignatureCallback; + return clone; + } + + #endregion + + #region IChannelBindingElement Methods + + /// <summary> + /// Signs the outgoing message. + /// </summary> + /// <param name="message">The message to sign.</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> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null && this.IsMessageApplicable(signedMessage)) { + if (this.SignatureCallback != null) { + this.SignatureCallback(signedMessage); + } else { + Logger.Bindings.Warn("Signing required, but callback delegate was not provided to provide additional data for signing."); + } + + signedMessage.SignatureMethod = this.signatureMethod; + Logger.Bindings.DebugFormat("Signing {0} message using {1}.", message.GetType().Name, this.signatureMethod); + signedMessage.Signature = this.GetSignature(signedMessage); + return MessageProtections.TamperProtection; + } + + return null; + } + + /// <summary> + /// Verifies the signature on an incoming message. + /// </summary> + /// <param name="message">The message whose signature should be verified.</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="InvalidSignatureException">Thrown if the signature is invalid.</exception> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var signedMessage = message as ITamperResistantOAuthMessage; + if (signedMessage != null && this.IsMessageApplicable(signedMessage)) { + Logger.Bindings.DebugFormat("Verifying incoming {0} message signature of: {1}", message.GetType().Name, signedMessage.Signature); + + if (!string.Equals(signedMessage.SignatureMethod, this.signatureMethod, StringComparison.Ordinal)) { + Logger.Bindings.WarnFormat("Expected signature method '{0}' but received message with a signature method of '{1}'.", this.signatureMethod, signedMessage.SignatureMethod); + return MessageProtections.None; + } + + if (this.SignatureCallback != null) { + this.SignatureCallback(signedMessage); + } else { + Logger.Bindings.Warn("Signature verification required, but callback delegate was not provided to provide additional data for signature verification."); + } + + if (!this.IsSignatureValid(signedMessage)) { + Logger.Bindings.Error("Signature verification failed."); + throw new InvalidSignatureException(message); + } + + return MessageProtections.TamperProtection; + } + + return null; + } + + #endregion + + /// <summary> + /// Constructs the OAuth Signature Base String and returns the result. + /// </summary> + /// <param name="message">The message.</param> + /// <param name="messageDictionary">The message to derive the signature base string from.</param> + /// <returns>The signature base string.</returns> + /// <remarks> + /// This method implements OAuth 1.0 section 9.1. + /// </remarks> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Unavoidable")] + internal static string ConstructSignatureBaseString(ITamperResistantOAuthMessage message, MessageDictionary messageDictionary) { + Requires.NotNull(message, "message"); + Requires.NotNullOrEmpty(message.HttpMethod, "message.HttpMethod"); + Requires.NotNull(messageDictionary, "messageDictionary"); + ErrorUtilities.VerifyArgument(messageDictionary.Message == message, "Message references are not equal."); + + List<string> signatureBaseStringElements = new List<string>(3); + + signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant()); + + // For multipart POST messages, only include the message parts that are NOT + // in the POST entity (those parts that may appear in an OAuth authorization header). + var encodedDictionary = new Dictionary<string, string>(); + IEnumerable<KeyValuePair<string, string>> partsToInclude = Enumerable.Empty<KeyValuePair<string, string>>(); + var binaryMessage = message as IMessageWithBinaryData; + if (binaryMessage != null && binaryMessage.SendAsMultipart) { + HttpDeliveryMethods authHeaderInUseFlags = HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest; + ErrorUtilities.VerifyProtocol((binaryMessage.HttpMethods & authHeaderInUseFlags) == authHeaderInUseFlags, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader); + + // Include the declared keys in the signature as those will be signable. + // Cache in local variable to avoid recalculating DeclaredKeys in the delegate. + ICollection<string> declaredKeys = messageDictionary.DeclaredKeys; + partsToInclude = messageDictionary.Where(pair => declaredKeys.Contains(pair.Key)); + } else { + partsToInclude = messageDictionary; + } + + // If this message was deserialized, include only those explicitly included message parts (excludes defaulted values) + // in the signature. + var originalPayloadMessage = (IMessageOriginalPayload)message; + if (originalPayloadMessage.OriginalPayload != null) { + partsToInclude = partsToInclude.Where(pair => originalPayloadMessage.OriginalPayload.ContainsKey(pair.Key)); + } + + foreach (var pair in OAuthChannel.GetUriEscapedParameters(partsToInclude)) { + encodedDictionary[pair.Key] = pair.Value; + } + + // An incoming message will already have included the query and form parameters + // in the message dictionary, but an outgoing message COULD have SOME parameters + // in the query that are not in the message dictionary because they were included + // in the receiving endpoint (the original URL). + // In an outgoing message, the POST entity can only contain parameters if they were + // in the message dictionary, so no need to pull out any parameters from there. + if (message.Recipient.Query != null) { + NameValueCollection nvc = HttpUtility.ParseQueryString(message.Recipient.Query); + foreach (string key in nvc) { + string escapedKey = MessagingUtilities.EscapeUriDataStringRfc3986(key); + string escapedValue = MessagingUtilities.EscapeUriDataStringRfc3986(nvc[key]); + string existingValue; + if (!encodedDictionary.TryGetValue(escapedKey, out existingValue)) { + encodedDictionary.Add(escapedKey, escapedValue); + } else { + ErrorUtilities.VerifyInternal(escapedValue == existingValue, "Somehow we have conflicting values for the '{0}' parameter.", escapedKey); + } + } + } + encodedDictionary.Remove("oauth_signature"); + + UriBuilder endpoint = new UriBuilder(message.Recipient); + endpoint.Query = null; + endpoint.Fragment = null; + signatureBaseStringElements.Add(endpoint.Uri.AbsoluteUri); + + var sortedKeyValueList = new List<KeyValuePair<string, string>>(encodedDictionary); + sortedKeyValueList.Sort(SignatureBaseStringParameterComparer); + StringBuilder paramBuilder = new StringBuilder(); + foreach (var pair in sortedKeyValueList) { + if (paramBuilder.Length > 0) { + paramBuilder.Append("&"); + } + + paramBuilder.Append(pair.Key); + paramBuilder.Append('='); + paramBuilder.Append(pair.Value); + } + + signatureBaseStringElements.Add(paramBuilder.ToString()); + + StringBuilder signatureBaseString = new StringBuilder(); + foreach (string element in signatureBaseStringElements) { + if (signatureBaseString.Length > 0) { + signatureBaseString.Append("&"); + } + + signatureBaseString.Append(MessagingUtilities.EscapeUriDataStringRfc3986(element)); + } + + Logger.Bindings.DebugFormat("Constructed signature base string: {0}", signatureBaseString); + return signatureBaseString.ToString(); + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + /// <remarks> + /// This method signs the message per OAuth 1.0 section 9.2. + /// </remarks> + internal string GetSignatureTestHook(ITamperResistantOAuthMessage message) { + return this.GetSignature(message); + } + + /// <summary> + /// Gets the "ConsumerSecret&TokenSecret" string, allowing either property to be empty or null. + /// </summary> + /// <param name="message">The message to extract the secrets from.</param> + /// <returns>The concatenated string.</returns> + protected static string GetConsumerAndTokenSecretString(ITamperResistantOAuthMessage message) { + StringBuilder builder = new StringBuilder(); + if (!string.IsNullOrEmpty(message.ConsumerSecret)) { + builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.ConsumerSecret)); + } + builder.Append("&"); + if (!string.IsNullOrEmpty(message.TokenSecret)) { + builder.Append(MessagingUtilities.EscapeUriDataStringRfc3986(message.TokenSecret)); + } + return builder.ToString(); + } + + /// <summary> + /// Determines whether the signature on some message is valid. + /// </summary> + /// <param name="message">The message to check the signature on.</param> + /// <returns> + /// <c>true</c> if the signature on the message is valid; otherwise, <c>false</c>. + /// </returns> + protected virtual bool IsSignatureValid(ITamperResistantOAuthMessage message) { + Requires.NotNull(message, "message"); + + string signature = this.GetSignature(message); + return MessagingUtilities.EqualsConstantTime(message.Signature, signature); + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + /// <remarks> + /// Implementations of this method need not clone the SignatureVerificationCallback member, as the + /// <see cref="SigningBindingElementBase"/> class does this. + /// </remarks> + protected abstract ITamperProtectionChannelBindingElement Clone(); + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + protected abstract string GetSignature(ITamperResistantOAuthMessage message); + + /// <summary> + /// Checks whether this binding element applies to this message. + /// </summary> + /// <param name="message">The message that needs to be signed.</param> + /// <returns>True if this binding element can be used to sign the message. False otherwise.</returns> + protected virtual bool IsMessageApplicable(ITamperResistantOAuthMessage message) { + return string.IsNullOrEmpty(message.SignatureMethod) || message.SignatureMethod == this.signatureMethod; + } + + /// <summary> + /// Sorts parameters according to OAuth signature base string rules. + /// </summary> + /// <param name="left">The first parameter to compare.</param> + /// <param name="right">The second parameter to compare.</param> + /// <returns>Negative, zero or positive.</returns> + private static int SignatureBaseStringParameterComparer(KeyValuePair<string, string> left, KeyValuePair<string, string> right) { + int result = string.CompareOrdinal(left.Key, right.Key); + if (result != 0) { + return result; + } + + return string.CompareOrdinal(left.Value, right.Value); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs new file mode 100644 index 0000000..4450fb5 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementBaseContract.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementBaseContract.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Code Contract for the <see cref="SigningBindingElementBase"/> class. + /// </summary> + [ContractClassFor(typeof(SigningBindingElementBase))] + internal abstract class SigningBindingElementBaseContract : SigningBindingElementBase { + /// <summary> + /// Prevents a default instance of the SigningBindingElementBaseContract class from being created. + /// </summary> + private SigningBindingElementBaseContract() + : base(string.Empty) { + } + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>A new instance of the binding element.</returns> + /// <remarks> + /// Implementations of this method need not clone the SignatureVerificationCallback member, as the + /// <see cref="SigningBindingElementBase"/> class does this. + /// </remarks> + protected override ITamperProtectionChannelBindingElement Clone() { + throw new NotImplementedException(); + } + + /// <summary> + /// Calculates a signature for a given message. + /// </summary> + /// <param name="message">The message to sign.</param> + /// <returns>The signature for the message.</returns> + protected override string GetSignature(ITamperResistantOAuthMessage message) { + Requires.NotNull(message, "message"); + Requires.ValidState(this.Channel != null); + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs new file mode 100644 index 0000000..849ad5e --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/SigningBindingElementChain.cs @@ -0,0 +1,142 @@ +//----------------------------------------------------------------------- +// <copyright file="SigningBindingElementChain.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; + + /// <summary> + /// A tamper protection applying binding element that can use any of several given + /// binding elements to apply the protection. + /// </summary> + internal class SigningBindingElementChain : ITamperProtectionChannelBindingElement { + /// <summary> + /// The various signing binding elements that may be applicable to a message in preferred use order. + /// </summary> + private readonly ITamperProtectionChannelBindingElement[] signers; + + /// <summary> + /// Initializes a new instance of the <see cref="SigningBindingElementChain"/> class. + /// </summary> + /// <param name="signers"> + /// The signing binding elements that may be used for some outgoing message, + /// in preferred use order. + /// </param> + internal SigningBindingElementChain(ITamperProtectionChannelBindingElement[] signers) { + Requires.NotNullOrEmpty(signers, "signers"); + Requires.NullOrWithNoNullElements(signers, "signers"); + Requires.True(signers.Select(s => s.Protection).Distinct().Count() == 1, "signers", OAuthStrings.SigningElementsMustShareSameProtection); + + this.signers = signers; + } + + #region ITamperProtectionChannelBindingElement Properties + + /// <summary> + /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a signed + /// message so that its signature can be correctly calculated for verification. + /// May be null for Consumers (who never have to verify signatures). + /// </summary> + public Action<ITamperResistantOAuthMessage> SignatureCallback { + get { + return this.signers[0].SignatureCallback; + } + + set { + foreach (ITamperProtectionChannelBindingElement signer in this.signers) { + signer.SignatureCallback = value; + } + } + } + + #endregion + + #region IChannelBindingElement Members + + /// <summary> + /// Gets the protection offered (if any) by this binding element. + /// </summary> + public MessageProtections Protection { + get { return this.signers[0].Protection; } + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + public Channel Channel { + get { + return this.signers[0].Channel; + } + + set { + foreach (var signer in this.signers) { + signer.Channel = value; + } + } + } + + /// <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> + public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + foreach (IChannelBindingElement signer in this.signers) { + ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null."); + MessageProtections? result = signer.ProcessOutgoingMessage(message); + if (result.HasValue) { + return result; + } + } + + 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> + public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + foreach (IChannelBindingElement signer in this.signers) { + ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null."); + MessageProtections? result = signer.ProcessIncomingMessage(message); + if (result.HasValue) { + return result; + } + } + + return null; + } + + #endregion + + #region ITamperProtectionChannelBindingElement Methods + + /// <summary> + /// Creates a new object that is a copy of the current instance. + /// </summary> + /// <returns> + /// A new object that is a copy of this instance. + /// </returns> + ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() { + return new SigningBindingElementChain(this.signers.Select(el => (ITamperProtectionChannelBindingElement)el.Clone()).ToArray()); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs new file mode 100644 index 0000000..46c2bd9 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/TokenType.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="TokenType.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + /// <summary> + /// The two types of tokens that exist in the OAuth protocol. + /// </summary> + public enum TokenType { + /// <summary> + /// A token that is freely issued to any known Consumer. + /// It does not grant any authorization to access protected resources, + /// but is used as a step in obtaining that access. + /// </summary> + RequestToken, + + /// <summary> + /// A token only obtained after the owner of some protected resource(s) + /// has approved a Consumer's access to said resource(s). + /// </summary> + AccessToken, + + /// <summary> + /// An unrecognized, expired or invalid token. + /// </summary> + InvalidToken, + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs new file mode 100644 index 0000000..287ef01 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/UriOrOobEncoding.cs @@ -0,0 +1,77 @@ +//----------------------------------------------------------------------- +// <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.Diagnostics.Contracts; + 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) { + 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/OAuth/ClassDiagram.cd b/src/DotNetOpenAuth.OAuth/OAuth/ClassDiagram.cd new file mode 100644 index 0000000..f56fd83 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ClassDiagram.cd @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1" MembersFormat="NameAndType"> + <Class Name="DotNetOpenAuth.DesktopConsumer"> + <Position X="11.5" Y="5.25" Width="4.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAEAAAAAAABAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\DesktopConsumer.cs</FileName> + <NewMemberFileName>WebConsumer.cs</NewMemberFileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.WebConsumer"> + <Position X="6.25" Y="5.25" Width="5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAA=</HashCode> + <FileName>OAuth\WebConsumer.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.ServiceProviderDescription"> + <Position X="0.5" Y="5.25" Width="5" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAACAAgAAAAEAAAQAAAAAAAAAAAAACAAAAAgAAAAAAA=</HashCode> + <FileName>OAuth\ServiceProviderDescription.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.ServiceProvider"> + <Position X="1" Y="0.5" Width="6" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>EAoAAAsgAAAAIAAAEAAAAAAAAAAIAAEAAQAIAAwAAAA=</HashCode> + <FileName>OAuth\ServiceProvider.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.ConsumerBase"> + <Position X="9" Y="0.5" Width="4.5" /> + <TypeIdentifier> + <HashCode>ACAAAAEgAAAAAAAAAAAABgAAAAAIAAAAAQAAAAABABA=</HashCode> + <FileName>OAuth\ConsumerBase.cs</FileName> + </TypeIdentifier> + </Class> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ConsumerSecuritySettings.cs b/src/DotNetOpenAuth.OAuth/OAuth/ConsumerSecuritySettings.cs new file mode 100644 index 0000000..bb2fbaa --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/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/OAuth/Messages/AccessProtectedResourceRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AccessProtectedResourceRequest.cs new file mode 100644 index 0000000..f3231f0 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AccessProtectedResourceRequest.cs @@ -0,0 +1,72 @@ +//----------------------------------------------------------------------- +// <copyright file="AccessProtectedResourceRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message attached to a request for protected resources that provides the necessary + /// credentials to be granted access to those resources. + /// </summary> + public class AccessProtectedResourceRequest : SignedMessageBase, ITokenContainingMessage, IMessageWithBinaryData { + /// <summary> + /// A store for the binary data that is carried in the message. + /// </summary> + private List<MultipartPostPart> binaryData = new List<MultipartPostPart>(); + + /// <summary> + /// 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> + /// <param name="version">The OAuth version.</param> + protected internal AccessProtectedResourceRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.AccessToken; } + set { this.AccessToken = value; } + } + + /// <summary> + /// Gets or sets the Access Token. + /// </summary> + /// <remarks> + /// In addition to just allowing OAuth to verify a valid message, + /// this property is useful on the Service Provider to verify that the access token + /// has proper authorization for the resource being requested, and to know the + /// context around which user provided the authorization. + /// </remarks> + [MessagePart("oauth_token", IsRequired = true)] + public string AccessToken { get; set; } + + #region IMessageWithBinaryData Members + + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + public IList<MultipartPostPart> BinaryData { + get { return this.binaryData; } + } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + public bool SendAsMultipart { + get { return this.HttpMethod == "POST" && this.BinaryData.Count > 0; } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenRequest.cs new file mode 100644 index 0000000..02c6c1d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenRequest.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizedTokenRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Globalization; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A direct message sent by the Consumer to exchange an authorized Request Token + /// for an Access Token and Token Secret. + /// </summary> + /// <remarks> + /// The class is sealed because the OAuth spec forbids adding parameters to this message. + /// </remarks> + public sealed class AuthorizedTokenRequest : SignedMessageBase, ITokenContainingMessage { + /// <summary> + /// 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> + /// <param name="version">The OAuth version.</param> + internal AuthorizedTokenRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageTransport.Direct, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the Token. + /// </summary> + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <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 authorized Request Token used to obtain authorization. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + internal string RequestToken { get; set; } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + if (this.ExtraData.Count > 0) { + throw new ProtocolException(string.Format(CultureInfo.CurrentCulture, OAuthStrings.MessageNotAllowedExtraParameters, GetType().Name)); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenResponse.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenResponse.cs new file mode 100644 index 0000000..0b14819 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/AuthorizedTokenResponse.cs @@ -0,0 +1,62 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizedTokenResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A direct message sent from Service Provider to Consumer in response to + /// a Consumer's <see cref="AuthorizedTokenRequest"/> request. + /// </summary> + public class AuthorizedTokenResponse : MessageBase, ITokenSecretContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizedTokenResponse"/> class. + /// </summary> + /// <param name="originatingRequest">The originating request.</param> + protected internal AuthorizedTokenResponse(AuthorizedTokenRequest originatingRequest) + : base(MessageProtections.None, originatingRequest, originatingRequest.Version) { + } + + /// <summary> + /// Gets or sets the Access Token assigned by the Service Provider. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + public string AccessToken { get; set; } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.AccessToken; } + set { this.AccessToken = value; } + } + + /// <summary> + /// Gets or sets the Request or Access Token secret. + /// </summary> + string ITokenSecretContainingMessage.TokenSecret { + get { return this.TokenSecret; } + set { this.TokenSecret = value; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + + /// <summary> + /// Gets or sets the Token Secret. + /// </summary> + [MessagePart("oauth_token_secret", IsRequired = true)] + protected internal string TokenSecret { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenContainingMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenContainingMessage.cs new file mode 100644 index 0000000..832b754 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenContainingMessage.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenContainingMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + /// <summary> + /// An interface implemented by all OAuth messages that have a request or access token property. + /// </summary> + public interface ITokenContainingMessage { + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + string Token { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenSecretContainingMessage.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenSecretContainingMessage.cs new file mode 100644 index 0000000..95809ec --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/ITokenSecretContainingMessage.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------- +// <copyright file="ITokenSecretContainingMessage.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + /// <summary> + /// An interface implemented by all OAuth messages that have a request or access token and secret properties. + /// </summary> + public interface ITokenSecretContainingMessage : ITokenContainingMessage { + /// <summary> + /// Gets or sets the Request or Access Token secret. + /// </summary> + string TokenSecret { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/MessageBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/MessageBase.cs new file mode 100644 index 0000000..faa4345 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/MessageBase.cs @@ -0,0 +1,281 @@ +//----------------------------------------------------------------------- +// <copyright file="MessageBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A base class for all OAuth messages. + /// </summary> + [Serializable] + public abstract class MessageBase : IDirectedProtocolMessage, IDirectResponseProtocolMessage { + /// <summary> + /// A store for extra name/value data pairs that are attached to this message. + /// </summary> + private Dictionary<string, string> extraData = new Dictionary<string, string>(); + + /// <summary> + /// Gets a value indicating whether signing this message is required. + /// </summary> + private MessageProtections protectionRequired; + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + private MessageTransport transport; + + /// <summary> + /// The URI to the remote endpoint to send this message to. + /// </summary> + private MessageReceivingEndpoint recipient; + + /// <summary> + /// Backing store for the <see cref="OriginatingRequest"/> properties. + /// </summary> + private IDirectedProtocolMessage originatingRequest; + + /// <summary> + /// Backing store for the <see cref="Incoming"/> properties. + /// </summary> + private bool incoming; + +#if DEBUG + /// <summary> + /// Initializes static members of the <see cref="MessageBase"/> class. + /// </summary> + static MessageBase() { + LowSecurityMode = true; + } +#endif + + /// <summary> + /// Initializes a new instance of the <see cref="MessageBase"/> class for direct response 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> + /// <param name="version">The OAuth version.</param> + protected MessageBase(MessageProtections protectionRequired, IDirectedProtocolMessage originatingRequest, Version version) { + Requires.NotNull(originatingRequest, "originatingRequest"); + Requires.NotNull(version, "version"); + + this.protectionRequired = protectionRequired; + this.transport = MessageTransport.Direct; + this.originatingRequest = originatingRequest; + this.Version = version; + } + + /// <summary> + /// Initializes a new instance of the <see cref="MessageBase"/> class for direct requests or indirect messages. + /// </summary> + /// <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> + /// <param name="version">The OAuth version.</param> + protected MessageBase(MessageProtections protectionRequired, MessageTransport transport, MessageReceivingEndpoint recipient, Version version) { + Requires.NotNull(recipient, "recipient"); + Requires.NotNull(version, "version"); + + this.protectionRequired = protectionRequired; + this.transport = transport; + this.recipient = recipient; + this.Version = version; + } + + #region IProtocolMessage Properties + + /// <summary> + /// Gets the version of the protocol this message is prepared to implement. + /// </summary> + Version IMessage.Version { + get { return this.Version; } + } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + MessageProtections IProtocolMessage.RequiredProtection { + get { return this.RequiredProtection; } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + MessageTransport IProtocolMessage.Transport { + get { return this.Transport; } + } + + /// <summary> + /// Gets the dictionary of additional name/value fields tacked on to this message. + /// </summary> + IDictionary<string, string> IMessage.ExtraData { + get { return this.ExtraData; } + } + + #endregion + + #region IDirectedProtocolMessage Members + + /// <summary> + /// Gets the URI to the Service Provider endpoint to send this message to. + /// </summary> + Uri IDirectedProtocolMessage.Recipient { + get { return this.recipient != null ? this.recipient.Location : null; } + } + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + HttpDeliveryMethods IDirectedProtocolMessage.HttpMethods { + get { return this.HttpMethods; } + } + + #endregion + + #region IDirectResponseProtocolMessage Members + + /// <summary> + /// Gets the originating request message that caused this response to be formed. + /// </summary> + IDirectedProtocolMessage IDirectResponseProtocolMessage.OriginatingRequest { + get { return this.originatingRequest; } + } + + #endregion + + /// <summary> + /// Gets or sets a value indicating whether security sensitive strings are + /// emitted from the ToString() method. + /// </summary> + internal static bool LowSecurityMode { get; set; } + + /// <summary> + /// Gets a value indicating whether this message was deserialized as an incoming message. + /// </summary> + protected internal bool Incoming { + get { return this.incoming; } + } + + /// <summary> + /// Gets the version of the protocol this message is prepared to implement. + /// </summary> + protected internal Version Version { get; private set; } + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + protected MessageProtections RequiredProtection { + get { return this.protectionRequired; } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + protected MessageTransport Transport { + get { return this.transport; } + } + + /// <summary> + /// Gets the dictionary of additional name/value fields tacked on to this message. + /// </summary> + protected IDictionary<string, string> ExtraData { + get { return this.extraData; } + } + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + protected HttpDeliveryMethods HttpMethods { + get { return this.recipient != null ? this.recipient.AllowedMethods : HttpDeliveryMethods.None; } + } + + /// <summary> + /// Gets or sets the URI to the Service Provider endpoint to send this message to. + /// </summary> + protected Uri Recipient { + get { + return this.recipient != null ? this.recipient.Location : null; + } + + set { + if (this.recipient != null) { + this.recipient = new MessageReceivingEndpoint(value, this.recipient.AllowedMethods); + } else if (value != null) { + throw new InvalidOperationException(); + } + } + } + + /// <summary> + /// Gets the originating request message that caused this response to be formed. + /// </summary> + protected IDirectedProtocolMessage OriginatingRequest { + get { return this.originatingRequest; } + } + + #region IProtocolMessage Methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + void IMessage.EnsureValidMessage() { + this.EnsureValidMessage(); + } + + #endregion + + /// <summary> + /// Returns a human-friendly string describing the message and all serializable properties. + /// </summary> + /// <param name="channel">The channel that will carry this message.</param> + /// <returns> + /// The string representation of this object. + /// </returns> + internal virtual string ToString(Channel channel) { + Requires.NotNull(channel, "channel"); + + StringBuilder builder = new StringBuilder(); + builder.AppendFormat(CultureInfo.InvariantCulture, "{0} message", GetType().Name); + if (this.recipient != null) { + builder.AppendFormat(CultureInfo.InvariantCulture, " as {0} to {1}", this.recipient.AllowedMethods, this.recipient.Location); + } + builder.AppendLine(); + MessageDictionary dictionary = channel.MessageDescriptions.GetAccessor(this); + foreach (var pair in dictionary) { + string value = pair.Value; + if (pair.Key == "oauth_signature" && !LowSecurityMode) { + value = "xxxxxxxxxxxxx (not shown)"; + } + builder.Append('\t'); + builder.Append(pair.Key); + builder.Append(": "); + builder.AppendLine(value); + } + + return builder.ToString(); + } + + /// <summary> + /// Sets a flag indicating that this message is received (as opposed to sent). + /// </summary> + internal void SetAsIncoming() { + this.incoming = true; + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + protected virtual void EnsureValidMessage() { } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/OAuth Messages.cd b/src/DotNetOpenAuth.OAuth/OAuth/Messages/OAuth Messages.cd new file mode 100644 index 0000000..f5526d2 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/OAuth Messages.cd @@ -0,0 +1,164 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1" GroupingSetting="Access"> + <Comment CommentText="Messages from Service Provider"> + <Position X="7.912" Y="0.715" Height="0.291" Width="2.02" /> + </Comment> + <Comment CommentText="Messages from Consumer"> + <Position X="4.36" Y="0.683" Height="0.291" Width="2.02" /> + </Comment> + <Class Name="DotNetOpenAuth.OAuth.Messages.UnauthorizedTokenRequest"> + <Position X="4.25" Y="1" Width="3" /> + <Compartments> + <Compartment Name="Internal" Collapsed="true" /> + <Compartment Name="Private" Collapsed="true" /> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\UnauthorizedTokenRequest.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.UnauthorizedTokenResponse"> + <Position X="7.5" Y="1.5" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + <Property Name="ITokenSecretContainingMessage.TokenSecret" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAEAAAAAAIAAIAAAIAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\UnauthorizedTokenResponse.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.SignedMessageBase"> + <Position X="0.5" Y="1.5" Width="3.5" /> + <Members> + <Property Name="ITamperResistantOAuthMessage.ConsumerSecret" Hidden="true" /> + <Property Name="ITamperResistantOAuthMessage.HttpMethod" Hidden="true" /> + <Property Name="ITamperResistantOAuthMessage.SignatureMethod" Hidden="true" /> + <Property Name="ITamperResistantOAuthMessage.TokenSecret" Hidden="true" /> + <Property Name="ITamperResistantProtocolMessage.Signature" Hidden="true" /> + </Members> + <TypeIdentifier> + <HashCode>AIAAFAAAAIAAAAACgICAAgAAAgAAIAQAAAEAIgAQAAE=</HashCode> + <FileName>OAuth\Messages\SignedMessageBase.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.UserAuthorizationRequest"> + <Position X="4.25" Y="3" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAAIAAAAAAIAAAAAAAQAA=</HashCode> + <FileName>OAuth\Messages\UserAuthorizationRequest.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.UserAuthorizationResponse"> + <Position X="7.5" Y="4.5" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\UserAuthorizationResponse.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.AuthorizedTokenResponse"> + <Position X="7.5" Y="6.25" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + <Property Name="ITokenSecretContainingMessage.TokenSecret" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAEAAAIAAAIAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\AuthorizedTokenResponse.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.AuthorizedTokenRequest"> + <Position X="4.25" Y="5.5" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AAAAAAACQAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\AuthorizedTokenRequest.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.AccessProtectedResourceRequest"> + <Position X="4.25" Y="7.75" Width="3" /> + <Members> + <Property Name="ITokenContainingMessage.Token" Hidden="true" /> + </Members> + <TypeIdentifier> + <HashCode>AAAAAAACAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\AccessProtectedResourceRequest.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="DotNetOpenAuth.OAuth.Messages.MessageBase"> + <Position X="11" Y="1" Width="3.5" /> + <Members> + <Field Name="extraData" Hidden="true" /> + <Property Name="IDirectedProtocolMessage.Recipient" Hidden="true" /> + <Method Name="IMessage.EnsureValidMessage" Hidden="true" /> + <Property Name="IMessage.ExtraData" Hidden="true" /> + <Property Name="IMessage.Version" Hidden="true" /> + <Property Name="IProtocolMessage.RequiredProtection" Hidden="true" /> + <Property Name="IProtocolMessage.Transport" Hidden="true" /> + <Field Name="protectionRequired" Hidden="true" /> + <Field Name="recipient" Hidden="true" /> + <Field Name="transport" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>ICAMAAQAYAAAgAkEAIAIAAYACgAEYBAAIECBACAAAAA=</HashCode> + <FileName>OAuth\Messages\MessageBase.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Interface Name="DotNetOpenAuth.ChannelElements.ITamperResistantOAuthMessage"> + <Position X="11.25" Y="5.25" Width="2.5" /> + <TypeIdentifier> + <HashCode>AIAAAAAAAAAAAAAAAIAAAgAAAAAAIAQAAAAAAAAAAAA=</HashCode> + <FileName>ChannelElements\ITamperResistantOAuthMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.OAuth.Messages.ITokenContainingMessage"> + <Position X="1" Y="6" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAA=</HashCode> + <FileName>OAuth\Messages\ITokenContainingMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="DotNetOpenAuth.OAuth.Messages.ITokenSecretContainingMessage"> + <Position X="1" Y="7.5" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAA=</HashCode> + <FileName>OAuth\Messages\ITokenSecretContainingMessage.cs</FileName> + </TypeIdentifier> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/SignedMessageBase.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/SignedMessageBase.cs new file mode 100644 index 0000000..6084fc0 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/SignedMessageBase.cs @@ -0,0 +1,197 @@ +//----------------------------------------------------------------------- +// <copyright file="SignedMessageBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A base class for all signed OAuth messages. + /// </summary> + public class SignedMessageBase : MessageBase, ITamperResistantOAuthMessage, IExpiringProtocolMessage, IReplayProtectedProtocolMessage { + /// <summary> + /// The reference date and time for calculating time stamps. + /// </summary> + private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// <summary> + /// The number of seconds since 1/1/1970, consistent with the OAuth timestamp requirement. + /// </summary> + [MessagePart("oauth_timestamp", IsRequired = true)] + private long timestamp; + + /// <summary> + /// Initializes a new instance of the <see cref="SignedMessageBase"/> class. + /// </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> + /// <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 = MessagingUtilities.GetHttpVerb(methods); + } + + #region ITamperResistantOAuthMessage Members + + /// <summary> + /// Gets or sets the signature method used to sign the request. + /// </summary> + string ITamperResistantOAuthMessage.SignatureMethod { + get { return this.SignatureMethod; } + set { this.SignatureMethod = value; } + } + + /// <summary> + /// Gets or sets the Token Secret used to sign the message. + /// </summary> + string ITamperResistantOAuthMessage.TokenSecret { + get { return this.TokenSecret; } + set { this.TokenSecret = value; } + } + + /// <summary> + /// Gets or sets the Consumer key. + /// </summary> + [MessagePart("oauth_consumer_key", IsRequired = true)] + public string ConsumerKey { get; set; } + + /// <summary> + /// Gets or sets the Consumer Secret used to sign the message. + /// </summary> + string ITamperResistantOAuthMessage.ConsumerSecret { + get { return this.ConsumerSecret; } + set { this.ConsumerSecret = value; } + } + + /// <summary> + /// Gets or sets the HTTP method that will be used to transmit the message. + /// </summary> + string ITamperResistantOAuthMessage.HttpMethod { + get { return this.HttpMethod; } + set { this.HttpMethod = value; } + } + + /// <summary> + /// Gets or sets the URI to the Service Provider endpoint to send this message to. + /// </summary> + Uri ITamperResistantOAuthMessage.Recipient { + get { return this.Recipient; } + set { this.Recipient = value; } + } + + #endregion + + #region ITamperResistantProtocolMessage Members + + /// <summary> + /// Gets or sets the message signature. + /// </summary> + string ITamperResistantProtocolMessage.Signature { + get { return this.Signature; } + set { this.Signature = value; } + } + + #endregion + + #region IExpiringProtocolMessage Members + + /// <summary> + /// Gets or sets the OAuth timestamp of the message. + /// </summary> + DateTime IExpiringProtocolMessage.UtcCreationDate { + get { return epoch + TimeSpan.FromSeconds(this.timestamp); } + set { this.timestamp = (long)(value - epoch).TotalSeconds; } + } + + #endregion + + #region IReplayProtectedProtocolMessage Members + + /// <summary> + /// Gets the context within which the nonce must be unique. + /// </summary> + /// <value>The consumer key.</value> + string IReplayProtectedProtocolMessage.NonceContext { + get { return this.ConsumerKey; } + } + + /// <summary> + /// Gets or sets the message nonce used for replay detection. + /// </summary> + [MessagePart("oauth_nonce", IsRequired = true)] + string IReplayProtectedProtocolMessage.Nonce { get; set; } + + #endregion + + #region IMessageOriginalPayload Members + + /// <summary> + /// Gets or sets the original message parts, before any normalization or default values were assigned. + /// </summary> + IDictionary<string, string> IMessageOriginalPayload.OriginalPayload { + get { return this.OriginalPayload; } + set { this.OriginalPayload = value; } + } + + /// <summary> + /// Gets or sets the original message parts, before any normalization or default values were assigned. + /// </summary> + [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "By design")] + protected IDictionary<string, string> OriginalPayload { get; set; } + + #endregion + + /// <summary> + /// Gets or sets the signature method used to sign the request. + /// </summary> + [MessagePart("oauth_signature_method", IsRequired = true)] + protected string SignatureMethod { get; set; } + + /// <summary> + /// Gets or sets the Token Secret used to sign the message. + /// </summary> + protected string TokenSecret { get; set; } + + /// <summary> + /// Gets or sets the Consumer Secret used to sign the message. + /// </summary> + protected string ConsumerSecret { get; set; } + + /// <summary> + /// Gets or sets the HTTP method that will be used to transmit the message. + /// </summary> + protected string HttpMethod { get; set; } + + /// <summary> + /// Gets or sets the message signature. + /// </summary> + [MessagePart("oauth_signature", IsRequired = true)] + protected string Signature { get; set; } + + /// <summary> + /// Gets or sets the version of the protocol this message was created with. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Accessed via reflection.")] + [MessagePart("oauth_version", IsRequired = false)] + private string OAuthVersion { + get { + return Protocol.Lookup(Version).PublishedVersion; + } + + set { + if (value != this.OAuthVersion) { + throw new ArgumentOutOfRangeException("value"); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenRequest.cs new file mode 100644 index 0000000..9214d91 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenRequest.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// <copyright file="UnauthorizedTokenRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +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. + /// </summary> + public class UnauthorizedTokenRequest : SignedMessageBase { + /// <summary> + /// 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> + /// <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 { + get { return base.ExtraData; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenResponse.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenResponse.cs new file mode 100644 index 0000000..04dee88 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UnauthorizedTokenResponse.cs @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------- +// <copyright file="UnauthorizedTokenResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A direct message sent from Service Provider to Consumer in response to + /// a Consumer's <see cref="UnauthorizedTokenRequest"/> request. + /// </summary> + public class UnauthorizedTokenResponse : MessageBase, ITokenSecretContainingMessage { + /// <summary> + /// Initializes a new instance of the <see cref="UnauthorizedTokenResponse"/> class. + /// </summary> + /// <param name="requestMessage">The unauthorized request token message that this message is being generated in response to.</param> + /// <param name="requestToken">The request token.</param> + /// <param name="tokenSecret">The token secret.</param> + /// <remarks> + /// This constructor is used by the Service Provider to send the message. + /// </remarks> + protected internal UnauthorizedTokenResponse(UnauthorizedTokenRequest requestMessage, string requestToken, string tokenSecret) + : this(requestMessage, requestMessage.Version) { + Requires.NotNull(requestToken, "requestToken"); + Requires.NotNull(tokenSecret, "tokenSecret"); + + this.RequestToken = requestToken; + this.TokenSecret = tokenSecret; + } + + /// <summary> + /// 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, Version version) + : base(MessageProtections.None, originatingRequest, version) { + } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <summary> + /// Gets or sets the Request or Access Token secret. + /// </summary> + string ITokenSecretContainingMessage.TokenSecret { + get { return this.TokenSecret; } + set { this.TokenSecret = value; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + + /// <summary> + /// Gets or sets the Request Token. + /// </summary> + [MessagePart("oauth_token", IsRequired = true)] + internal string RequestToken { get; set; } + + /// <summary> + /// Gets the original request for an unauthorized token. + /// </summary> + internal UnauthorizedTokenRequest RequestMessage { + get { return (UnauthorizedTokenRequest)this.OriginatingRequest; } + } + + /// <summary> + /// Gets or sets the Token Secret. + /// </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> + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Message serialization invoked.")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Message parts must be instance members.")] + [MessagePart("oauth_callback_confirmed", IsRequired = true, MinVersion = Protocol.V10aVersion)] + private bool CallbackConfirmed { + get { return true; } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationRequest.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationRequest.cs new file mode 100644 index 0000000..a5823bb --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationRequest.cs @@ -0,0 +1,82 @@ +//----------------------------------------------------------------------- +// <copyright file="UserAuthorizationRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message used to redirect the user from a Consumer to a Service Provider's web site + /// so the Service Provider can ask the user to authorize the Consumer's access to some + /// protected resource(s). + /// </summary> + [Serializable] + public class UserAuthorizationRequest : MessageBase, ITokenContainingMessage { + /// <summary> + /// 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> + /// <param name="requestToken">The request token.</param> + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, string requestToken, Version version) + : this(serviceProvider, version) { + this.RequestToken = requestToken; + } + + /// <summary> + /// 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> + /// <param name="version">The OAuth version.</param> + internal UserAuthorizationRequest(MessageReceivingEndpoint serviceProvider, Version version) + : base(MessageProtections.None, MessageTransport.Indirect, serviceProvider, version) { + } + + /// <summary> + /// Gets or sets the Request or Access Token. + /// </summary> + [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "This property IS accessible by a different name.")] + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <summary> + /// Gets the extra, non-OAuth parameters that will be included in the message. + /// </summary> + public new IDictionary<string, string> ExtraData { + get { return base.ExtraData; } + } + + /// <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> + /// The Service Provider MAY declare this parameter as REQUIRED, or + /// accept requests to the User Authorization URL without it, in which + /// case it will prompt the User to enter it manually. + /// </remarks> + [MessagePart("oauth_token", IsRequired = false)] + internal string RequestToken { get; set; } + + /// <summary> + /// 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, MaxVersion = "1.0")] + internal Uri Callback { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationResponse.cs b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationResponse.cs new file mode 100644 index 0000000..73fddc7 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Messages/UserAuthorizationResponse.cs @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------- +// <copyright file="UserAuthorizationResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.Messages { + using System; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A message used to redirect the user from a Service Provider to a Consumer's web site. + /// </summary> + /// <remarks> + /// The class is sealed because extra parameters are determined by the callback URI provided by the Consumer. + /// </remarks> + [Serializable] + public sealed class UserAuthorizationResponse : MessageBase, ITokenContainingMessage { + /// <summary> + /// 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> + /// <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> + /// Gets or sets the Request or Access Token. + /// </summary> + string ITokenContainingMessage.Token { + get { return this.RequestToken; } + set { this.RequestToken = value; } + } + + /// <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)] + internal string RequestToken { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.Designer.cs b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.Designer.cs new file mode 100644 index 0000000..723839d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.Designer.cs @@ -0,0 +1,198 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.225 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.OAuth { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class OAuthStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal OAuthStrings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.OAuth.OAuthStrings", typeof(OAuthStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot send access token to Consumer for request token '{0}' before it has been authorized.. + /// </summary> + internal static string AccessTokenNotAuthorized { + get { + return ResourceManager.GetString("AccessTokenNotAuthorized", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The access token '{0}' is invalid or expired.. + /// </summary> + internal static string BadAccessTokenInProtectedResourceRequest { + get { + return ResourceManager.GetString("BadAccessTokenInProtectedResourceRequest", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failure looking up secret for consumer or token.. + /// </summary> + internal static string ConsumerOrTokenSecretNotFound { + get { + return ResourceManager.GetString("ConsumerOrTokenSecretNotFound", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to oauth_verifier argument was incorrect.. + /// </summary> + internal static string IncorrectVerifier { + get { + return ResourceManager.GetString("IncorrectVerifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An invalid OAuth message received and discarded.. + /// </summary> + internal static string InvalidIncomingMessage { + get { + return ResourceManager.GetString("InvalidIncomingMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The {0} message included extra data which is not allowed.. + /// </summary> + internal static string MessageNotAllowedExtraParameters { + get { + return ResourceManager.GetString("MessageNotAllowedExtraParameters", resourceCulture); + } + } + + /// <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 Cannot send OAuth message as multipart POST without an authorization HTTP header because sensitive data would not be signed.. + /// </summary> + internal static string MultipartPostMustBeUsedWithAuthHeader { + get { + return ResourceManager.GetString("MultipartPostMustBeUsedWithAuthHeader", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.. + /// </summary> + internal static string OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface { + get { + return ResourceManager.GetString("OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The OpenID Relying Party's realm is not recognized as belonging to the OAuth Consumer identified by the consumer key given.. + /// </summary> + internal static string OpenIdOAuthRealmConsumerKeyDoNotMatch { + get { + return ResourceManager.GetString("OpenIdOAuthRealmConsumerKeyDoNotMatch", 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 { + get { + return ResourceManager.GetString("RequestUrlMustNotHaveOAuthParameters", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The signing element already has been associated with a channel.. + /// </summary> + internal static string SigningElementAlreadyAssociatedWithChannel { + get { + return ResourceManager.GetString("SigningElementAlreadyAssociatedWithChannel", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to All signing elements must offer the same message protection.. + /// </summary> + internal static string SigningElementsMustShareSameProtection { + get { + return ResourceManager.GetString("SigningElementsMustShareSameProtection", resourceCulture); + } + } + + /// <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 { + get { + return ResourceManager.GetString("X509CertificateNotProvidedForSigning", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.resx b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.resx new file mode 100644 index 0000000..34b314b --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.resx @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AccessTokenNotAuthorized" xml:space="preserve"> + <value>Cannot send access token to Consumer for request token '{0}' before it has been authorized.</value> + </data> + <data name="BadAccessTokenInProtectedResourceRequest" xml:space="preserve"> + <value>The access token '{0}' is invalid or expired.</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="MultipartPostMustBeUsedWithAuthHeader" xml:space="preserve"> + <value>Cannot send OAuth message as multipart POST without an authorization HTTP header because sensitive data would not be signed.</value> + </data> + <data name="OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface" xml:space="preserve"> + <value>Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.</value> + </data> + <data name="OpenIdOAuthRealmConsumerKeyDoNotMatch" xml:space="preserve"> + <value>The OpenID Relying Party's realm is not recognized as belonging to the OAuth Consumer identified by the consumer key given.</value> + </data> + <data name="RequestUrlMustNotHaveOAuthParameters" xml:space="preserve"> + <value>The request URL query MUST NOT contain any OAuth Protocol Parameters.</value> + </data> + <data name="SigningElementAlreadyAssociatedWithChannel" xml:space="preserve"> + <value>The signing element already has been associated with a channel.</value> + </data> + <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> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.sr.resx b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.sr.resx new file mode 100644 index 0000000..ef9ce60 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/OAuthStrings.sr.resx @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="AccessTokenNotAuthorized" xml:space="preserve"> + <value>Ne može se poslati pristupni token za Consumera za token zahteva '{0}' pre nego što bude autorizovan.</value> + </data> + <data name="BadAccessTokenInProtectedResourceRequest" xml:space="preserve"> + <value>Pristupni token '{0}' je neispravan ili je mu je istekao rok važenja.</value> + </data> + <data name="ConsumerOrTokenSecretNotFound" xml:space="preserve"> + <value>Greška u traženju šifre za korisnika ili za token.</value> + </data> + <data name="IncorrectVerifier" xml:space="preserve"> + <value>oauth_verifier argument je neispravan.</value> + </data> + <data name="InvalidIncomingMessage" xml:space="preserve"> + <value>Neispravna OAuth poruka je primljena i odbačena.</value> + </data> + <data name="MessageNotAllowedExtraParameters" xml:space="preserve"> + <value>Poruka {0} je sadržala višak podataka koji nije dozvoljen.</value> + </data> + <data name="MinimumConsumerVersionRequirementNotMet" xml:space="preserve"> + <value>Ovaj provajder OAuth usluge zahteva od OAuth korisnika da da implementira OAuth {0}, ali izgleda da korisnika podržava jedino {1}.</value> + </data> + <data name="OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface" xml:space="preserve"> + <value>Korišćenje OpenID+OAuth ekstenzije zahteva da token menadžer koji se koristi implementira {0} interfejs.</value> + </data> + <data name="OpenIdOAuthRealmConsumerKeyDoNotMatch" xml:space="preserve"> + <value>OpenID Relying Party domen nije prepoznat kao da pripada OAuth Consumeru identifikovanom po datom korisničkom tokenu.</value> + </data> + <data name="RequestUrlMustNotHaveOAuthParameters" xml:space="preserve"> + <value>URL upit zahteva NE SME sadržati bilo kakve OAuth Protocol Parametre.</value> + </data> + <data name="SigningElementAlreadyAssociatedWithChannel" xml:space="preserve"> + <value>Potpisujući element je već asociran sa kanalom.</value> + </data> + <data name="SigningElementsMustShareSameProtection" xml:space="preserve"> + <value>Svi elementi za potpisivanje moraju nuditi istu zaštitu poruke.</value> + </data> + <data name="TokenNotFound" xml:space="preserve"> + <value>Token u poruci nije prepoznat od strane provajdera usluge.</value> + </data> + <data name="X509CertificateNotProvidedForSigning" xml:space="preserve"> + <value>RSA-SHA1 potpisujući povezujući element nije podešen sa sertifikatom za potpisivanje.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth/OAuth/Protocol.cs b/src/DotNetOpenAuth.OAuth/OAuth/Protocol.cs new file mode 100644 index 0000000..0b7aaa9 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/Protocol.cs @@ -0,0 +1,148 @@ +//----------------------------------------------------------------------- +// <copyright file="Protocol.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + 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> + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "a", Justification = "By design.")] + 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. + /// </summary> + 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 OAuth 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> + /// Initializes a new instance of the <see cref="Protocol"/> class. + /// </summary> + internal Protocol() { + this.PublishedVersion = "1.0"; + } + + /// <summary> + /// Gets the OAuth version this instance represents. + /// </summary> + internal Version Version { get; private set; } + + /// <summary> + /// Gets the version to declare on the wire. + /// </summary> + internal string PublishedVersion { get; private set; } + + /// <summary> + /// Gets the <see cref="ProtocolVersion"/> enum value for the <see cref="Protocol"/> instance. + /// </summary> + internal ProtocolVersion ProtocolVersion { get; private set; } + + /// <summary> + /// Gets the namespace to use for this version of the protocol. + /// </summary> + internal string DataContractNamespace { + get { return this.dataContractNamespace; } + } + + /// <summary> + /// Gets the OAuth Protocol instance to use for the given version. + /// </summary> + /// <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 the OAuth Protocol instance to use for the given version. + /// </summary> + /// <param name="version">The OAuth version to get.</param> + /// <returns>A matching <see cref="Protocol"/> instance.</returns> + internal static Protocol Lookup(Version version) { + Requires.NotNull(version, "version"); + Requires.InRange(AllVersions.Any(p => p.Version == version), "version"); + return AllVersions.First(p => p.Version == version); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/SecuritySettings.cs b/src/DotNetOpenAuth.OAuth/OAuth/SecuritySettings.cs new file mode 100644 index 0000000..3329f09 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/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/OAuth/ServiceProviderDescription.cs b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderDescription.cs new file mode 100644 index 0000000..6205f55 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderDescription.cs @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProviderDescription.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.ChannelElements; + + /// <summary> + /// A description of the endpoints on a Service Provider. + /// </summary> + public class ServiceProviderDescription { + /// <summary> + /// The field used to store the value of the <see cref="RequestTokenEndpoint"/> property. + /// </summary> + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private MessageReceivingEndpoint requestTokenEndpoint; + + /// <summary> + /// 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> + /// <remarks> + /// The request URL query MUST NOT contain any OAuth Protocol Parameters. + /// This is the URL that <see cref="OAuth.Messages.UnauthorizedTokenRequest"/> messages are directed to. + /// </remarks> + /// <exception cref="ArgumentException">Thrown if this property is set to a URI with OAuth protocol parameters.</exception> + public MessageReceivingEndpoint RequestTokenEndpoint { + get { + return this.requestTokenEndpoint; + } + + set { + if (value != null && UriUtil.QueryStringContainPrefixedParameters(value.Location, OAuth.Protocol.ParameterPrefix)) { + throw new ArgumentException(OAuthStrings.RequestUrlMustNotHaveOAuthParameters); + } + + this.requestTokenEndpoint = value; + } + } + + /// <summary> + /// Gets or sets the URL used to obtain User authorization for Consumer access, + /// described in Section 6.2 (Obtaining User Authorization). + /// </summary> + /// <remarks> + /// This is the URL that <see cref="OAuth.Messages.UserAuthorizationRequest"/> messages are + /// indirectly (via the user agent) sent to. + /// </remarks> + public MessageReceivingEndpoint UserAuthorizationEndpoint { get; set; } + + /// <summary> + /// Gets or sets the URL used to exchange the User-authorized Request Token + /// for an Access Token, described in Section 6.3 (Obtaining an Access Token). + /// </summary> + /// <remarks> + /// This is the URL that <see cref="OAuth.Messages.AuthorizedTokenRequest"/> messages are directed to. + /// </remarks> + public MessageReceivingEndpoint AccessTokenEndpoint { get; set; } + + /// <summary> + /// Gets or sets the signing policies that apply to this Service Provider. + /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Type initializers require this format.")] + 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> + internal ITamperProtectionChannelBindingElement CreateTamperProtectionElement() { + Contract.Requires(this.TamperProtectionElements != null); + return new SigningBindingElementChain(this.TamperProtectionElements.Select(el => (ITamperProtectionChannelBindingElement)el.Clone()).ToArray()); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderSecuritySettings.cs b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderSecuritySettings.cs new file mode 100644 index 0000000..701e36c --- /dev/null +++ b/src/DotNetOpenAuth.OAuth/OAuth/ServiceProviderSecuritySettings.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProviderSecuritySettings.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + + /// <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; } + + /// <summary> + /// Gets or sets the maximum time a user can take to complete authorization. + /// </summary> + /// <remarks> + /// This time limit serves as a security mitigation against brute force attacks to + /// compromise (unauthorized or authorized) request tokens. + /// Longer time limits is more friendly to slow users or consumers, while shorter + /// time limits provide better security. + /// </remarks> + public TimeSpan MaximumRequestTokenTimeToLive { get; set; } + } +} |