//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth.ChannelElements { using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Mime; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.Messaging.Reflection; using DotNetOpenAuth.OAuth.Messages; using Validation; using HttpRequestHeaders = DotNetOpenAuth.Messaging.HttpRequestHeaders; /// /// An OAuth-specific implementation of the class. /// internal abstract class OAuthChannel : Channel { /// /// Initializes a new instance of the class. /// /// The binding element to use for signing. /// The ITokenManager instance to use. /// The security settings. /// An injected message type provider instance. /// Except for mock testing, this should always be one of /// OAuthConsumerMessageFactory or OAuthServiceProviderMessageFactory. /// The binding elements. /// The host factories. [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires(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, ITokenManager tokenManager, SecuritySettings securitySettings, IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements, IHostFactories hostFactories = null) : base(messageTypeProvider, bindingElements, hostFactories ?? new DefaultOAuthHostFactories()) { Requires.NotNull(tokenManager, "tokenManager"); Requires.NotNull(securitySettings, "securitySettings"); Requires.NotNull(signingBindingElement, "signingBindingElement"); Requires.That(signingBindingElement.SignatureCallback == null, "signingBindingElement", OAuthStrings.SigningElementAlreadyAssociatedWithChannel); Requires.NotNull(bindingElements, "bindingElements"); this.TokenManager = tokenManager; signingBindingElement.SignatureCallback = this.SignatureCallback; } /// /// Gets or sets the Consumer web application path. /// internal Uri Realm { get; set; } /// /// Gets the token manager being used. /// protected internal ITokenManager TokenManager { get; private set; } /// /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1. /// /// The message with data to encode. /// A dictionary of name-value pairs with their strings encoded. internal static IDictionary GetUriEscapedParameters(IEnumerable> message) { var encodedDictionary = new Dictionary(); UriEscapeParameters(message, encodedDictionary); return encodedDictionary; } /// /// 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. /// /// The message to attach. /// The cancellation token. /// The initialized web request. internal async Task InitializeRequestAsync(IDirectedProtocolMessage request, CancellationToken cancellationToken) { Requires.NotNull(request, "request"); await this.ProcessOutgoingMessageAsync(request, cancellationToken); return this.CreateHttpRequest(request); } /// /// Initializes the binding elements for the OAuth channel. /// /// The signing binding element. /// The nonce store. /// /// An array of binding elements used to initialize the channel. /// protected static List InitializeBindingElements(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store) { var bindingElements = new List { new OAuthHttpMethodBindingElement(), signingBindingElement, new StandardExpirationBindingElement(), new StandardReplayProtectionBindingElement(store), }; return bindingElements; } /// /// Searches an incoming HTTP request for data that could be used to assemble /// a protocol request message. /// /// The HTTP request to search. /// The cancellation token. /// The deserialized message, if one is found. Null otherwise. protected override async Task ReadFromRequestCoreAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // First search the Authorization header. var authorization = request.Headers.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 foreach (var pair in await ParseUrlEncodedFormContentAsync(request, cancellationToken)) { if (pair.Key != null) { fields.Add(pair.Key, pair.Value); } else { Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", pair.Value); } } // Scrape the query string var qs = HttpUtility.ParseQueryString(request.RequestUri.Query); foreach (string key in qs) { if (key != null) { fields.Add(key, qs[key]); } else { Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", qs[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.RequestUri; signedMessage.HttpMethod = request.Method; } return message; } /// /// Gets the protocol message that may be in the given HTTP response. /// /// The response that is anticipated to contain an protocol message. /// The cancellation token. /// /// The deserialized message parts, if found. Null otherwise. /// protected override async Task> ReadFromResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken) { string body = await response.Content.ReadAsStringAsync(); return HttpUtility.ParseQueryString(body).ToDictionary(); } /// /// Prepares an HTTP request that carries a given message. /// /// The message to send. /// /// The prepared to send the request. /// protected override HttpRequestMessage CreateHttpRequest(IDirectedProtocolMessage request) { HttpRequestMessage 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; } /// /// Queues a message for sending in the response stream where the fields /// are sent in the response stream in querystring style. /// /// The message to send as a response. /// The pending user agent redirect based message to be sent as an HttpResponse. /// /// This method implements spec V1.0 section 5.3. /// protected override HttpResponseMessage PrepareDirectResponse(IProtocolMessage response) { var messageAccessor = this.MessageDescriptions.GetAccessor(response); var fields = messageAccessor.Serialize(); var encodedResponse = new HttpResponseMessage { Content = new FormUrlEncodedContent(fields), }; ApplyMessageTemplate(response, encodedResponse); return encodedResponse; } /// /// Gets the consumer secret for a given consumer key. /// /// The consumer key. /// A consumer secret. protected abstract string GetConsumerSecret(string consumerKey); /// /// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1. /// /// The dictionary with names and values to encode. /// The dictionary to add the encoded pairs to. private static void UriEscapeParameters(IEnumerable> source, IDictionary 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); } } /// /// Gets the HTTP method to use for a message. /// /// The message. /// "POST", "GET" or some other similar http verb. private static HttpMethod GetHttpMethod(IDirectedProtocolMessage message) { Requires.NotNull(message, "message"); var signedMessage = message as ITamperResistantOAuthMessage; if (signedMessage != null) { return signedMessage.HttpMethod; } else { return MessagingUtilities.GetHttpVerb(message.HttpMethods); } } /// /// Prepares to send a request to the Service Provider via the Authorization header. /// /// The message to be transmitted to the ServiceProvider. /// The web request ready to send. /// /// This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4). /// private HttpRequestMessage InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) { var dictionary = this.MessageDescriptions.GetAccessor(requestMessage); // copy so as to not modify original var fields = new Dictionary(); foreach (string key in dictionary.DeclaredKeys) { fields.Add(key, dictionary[key]); } if (this.Realm != null) { fields.Add("realm", this.Realm.AbsoluteUri); } UriBuilder recipientBuilder = new UriBuilder(requestMessage.Recipient); bool hasEntity = HttpMethodHasEntity(GetHttpMethod(requestMessage)); if (!hasEntity) { MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData); } var httpRequest = new HttpRequestMessage(GetHttpMethod(requestMessage), recipientBuilder.Uri); this.PrepareHttpWebRequest(httpRequest); httpRequest.Headers.Authorization = new AuthenticationHeaderValue(Protocol.AuthorizationHeaderScheme, MessagingUtilities.AssembleAuthorizationHeader(fields)); if (hasEntity) { 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 content = InitializeMultipartFormDataContent(requestMessageWithBinaryData); httpRequest.Content = content; foreach (var extraData in requestMessage.ExtraData) { content.Add(new StringContent(extraData.Value), extraData.Key); } } else { ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); if (requestMessage.ExtraData.Count > 0) { httpRequest.Content = new FormUrlEncodedContent(requestMessage.ExtraData); } } } return httpRequest; } /// /// Fills out the secrets in a message so that signing/verification can be performed. /// /// The message about to be signed or whose signature is about to be verified. 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); } } } }