//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth.ChannelElements; using DotNetOpenAuth.OAuth.Messages; using Validation; /// /// Provides OAuth 1.0 consumer services to a client or web application. /// public class Consumer { /// /// The host factories. /// private IHostFactories hostFactories; /// /// Initializes a new instance of the class. /// public Consumer() { this.HostFactories = new DefaultOAuthHostFactories(); } /// /// Initializes a new instance of the class. /// /// The consumer key. /// The consumer secret. /// The service provider. /// The temporary credential storage. /// The host factories. public Consumer( string consumerKey, string consumerSecret, ServiceProviderDescription serviceProvider, ITemporaryCredentialStorage temporaryCredentialStorage, IHostFactories hostFactories = null) { this.ConsumerKey = consumerKey; this.ConsumerSecret = consumerSecret; this.ServiceProvider = serviceProvider; this.TemporaryCredentialStorage = temporaryCredentialStorage; this.HostFactories = hostFactories ?? new DefaultOAuthHostFactories(); } /// /// Gets or sets the object with factories for host-customizable services. /// public IHostFactories HostFactories { get { return this.hostFactories; } set { Requires.NotNull(value, "value"); this.hostFactories = value; } } /// /// Gets or sets the Consumer Key used to communicate with the Service Provider. /// public string ConsumerKey { get; set; } /// /// Gets or sets the consumer secret. /// /// /// The consumer secret. /// public string ConsumerSecret { get; set; } /// /// Gets or sets the consumer certificate. /// /// /// The consumer certificate. /// /// /// If set, this causes all outgoing messages to be signed with the certificate instead of the consumer secret. /// public X509Certificate2 ConsumerCertificate { get; set; } /// /// Gets or sets the Service Provider that will be accessed. /// public ServiceProviderDescription ServiceProvider { get; set; } /// /// Gets or sets the persistence store for tokens and secrets. /// public ITemporaryCredentialStorage TemporaryCredentialStorage { get; set; } /// /// Obtains an access token for a new account at the Service Provider via 2-legged OAuth. /// /// Any applicable parameters to include in the query string of the token request. /// The cancellation token. /// The access token. public async Task RequestNewClientAccountAsync(IEnumerable> requestParameters = null, CancellationToken cancellationToken = default(CancellationToken)) { Verify.Operation(this.ConsumerKey != null, Strings.RequiredPropertyNotYetPreset, "ConsumerKey"); Verify.Operation(this.ServiceProvider != null, Strings.RequiredPropertyNotYetPreset, "ServiceProvider"); using (var handler = this.CreateMessageHandler()) { using (var client = this.CreateHttpClient(handler)) { string identifier, secret; var requestUri = new UriBuilder(this.ServiceProvider.TemporaryCredentialsRequestEndpoint); requestUri.AppendQueryArgument(Protocol.CallbackParameter, "oob"); requestUri.AppendQueryArgs(requestParameters); var request = new HttpRequestMessage(this.ServiceProvider.TemporaryCredentialsRequestEndpointMethod, requestUri.Uri); using (var response = await client.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); cancellationToken.ThrowIfCancellationRequested(); // Parse the response and ensure that it meets the requirements of the OAuth 1.0 spec. string content = await response.Content.ReadAsStringAsync(); var responseData = HttpUtility.ParseQueryString(content); identifier = responseData[Protocol.TokenParameter]; secret = responseData[Protocol.TokenSecretParameter]; ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(identifier), MessagingStrings.RequiredParametersMissing, typeof(UnauthorizedTokenResponse).Name, Protocol.TokenParameter); ErrorUtilities.VerifyProtocol(secret != null, MessagingStrings.RequiredParametersMissing, typeof(UnauthorizedTokenResponse).Name, Protocol.TokenSecretParameter); } // Immediately exchange the temporary credential for an access token. handler.AccessToken = identifier; handler.AccessTokenSecret = secret; request = new HttpRequestMessage(this.ServiceProvider.TokenRequestEndpointMethod, this.ServiceProvider.TokenRequestEndpoint); using (var response = await client.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); // Parse the response and ensure that it meets the requirements of the OAuth 1.0 spec. string content = await response.Content.ReadAsStringAsync(); var responseData = HttpUtility.ParseQueryString(content); string accessToken = responseData[Protocol.TokenParameter]; string tokenSecret = responseData[Protocol.TokenSecretParameter]; ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(accessToken), MessagingStrings.RequiredParametersMissing, typeof(AuthorizedTokenResponse).Name, Protocol.TokenParameter); ErrorUtilities.VerifyProtocol(tokenSecret != null, MessagingStrings.RequiredParametersMissing, typeof(AuthorizedTokenResponse).Name, Protocol.TokenSecretParameter); responseData.Remove(Protocol.TokenParameter); responseData.Remove(Protocol.TokenSecretParameter); return new AccessTokenResponse(accessToken, tokenSecret, responseData); } } } } /// /// Prepares an OAuth message that begins an authorization request that will /// redirect the user to the Service Provider to provide that authorization. /// /// The absolute URI that the Service Provider should redirect the /// User Agent to upon successful authorization, or null to signify an out of band return. /// Extra parameters to add to the request token message. Optional. /// The cancellation token. /// /// The URL to direct the user agent to for user authorization. /// public async Task RequestUserAuthorizationAsync(Uri callback = null, IEnumerable> requestParameters = null, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(callback, "callback"); Verify.Operation(this.ConsumerKey != null, Strings.RequiredPropertyNotYetPreset, "ConsumerKey"); Verify.Operation(this.TemporaryCredentialStorage != null, Strings.RequiredPropertyNotYetPreset, "TemporaryCredentialStorage"); Verify.Operation(this.ServiceProvider != null, Strings.RequiredPropertyNotYetPreset, "ServiceProvider"); // Obtain temporary credentials before the redirect. using (var client = this.CreateHttpClient(new AccessToken())) { var requestUri = new UriBuilder(this.ServiceProvider.TemporaryCredentialsRequestEndpoint); requestUri.AppendQueryArgument(Protocol.CallbackParameter, callback != null ? callback.AbsoluteUri : "oob"); requestUri.AppendQueryArgs(requestParameters); var request = new HttpRequestMessage(this.ServiceProvider.TemporaryCredentialsRequestEndpointMethod, requestUri.Uri); using (var response = await client.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); cancellationToken.ThrowIfCancellationRequested(); // Parse the response and ensure that it meets the requirements of the OAuth 1.0 spec. string content = await response.Content.ReadAsStringAsync(); var responseData = HttpUtility.ParseQueryString(content); ErrorUtilities.VerifyProtocol(string.Equals(responseData[Protocol.CallbackConfirmedParameter], "true", StringComparison.Ordinal), MessagingStrings.RequiredParametersMissing, typeof(UnauthorizedTokenResponse).Name, Protocol.CallbackConfirmedParameter); string identifier = responseData[Protocol.TokenParameter]; string secret = responseData[Protocol.TokenSecretParameter]; ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(identifier), MessagingStrings.RequiredParametersMissing, typeof(UnauthorizedTokenResponse).Name, Protocol.TokenParameter); ErrorUtilities.VerifyProtocol(secret != null, MessagingStrings.RequiredParametersMissing, typeof(UnauthorizedTokenResponse).Name, Protocol.TokenSecretParameter); // Save the temporary credential we received so that after user authorization // we can use it to obtain the access token. cancellationToken.ThrowIfCancellationRequested(); this.TemporaryCredentialStorage.SaveTemporaryCredential(identifier, secret); // Construct the user authorization URL so our caller can direct a browser to it. var authorizationEndpoint = new UriBuilder(this.ServiceProvider.ResourceOwnerAuthorizationEndpoint); authorizationEndpoint.AppendQueryArgument(Protocol.TokenParameter, identifier); return authorizationEndpoint.Uri; } } } /// /// Obtains an access token after a successful user authorization. /// /// The URI used to redirect back to the consumer that contains a message from the service provider. /// The cancellation token. /// /// The access token assigned by the Service Provider, or null if no response was detected in the specified URL. /// public async Task ProcessUserAuthorizationAsync(Uri authorizationCompleteUri, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(authorizationCompleteUri, "authorizationCompleteUri"); Verify.Operation(this.TemporaryCredentialStorage != null, Strings.RequiredPropertyNotYetPreset, "TemporaryCredentialStorage"); // Parse the response and verify that it meets spec requirements. var queryString = HttpUtility.ParseQueryString(authorizationCompleteUri.Query); string identifier = queryString[Protocol.TokenParameter]; string verifier = queryString[Protocol.VerifierParameter]; if (identifier == null) { // We assume there is no response message here at all, and return null to indicate that. return null; } ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(identifier), MessagingStrings.RequiredNonEmptyParameterWasEmpty, typeof(UserAuthorizationResponse).Name, Protocol.TokenParameter); ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(verifier), MessagingStrings.RequiredNonEmptyParameterWasEmpty, typeof(UserAuthorizationResponse).Name, Protocol.VerifierParameter); var temporaryCredential = this.TemporaryCredentialStorage.RetrieveTemporaryCredential(); Verify.Operation(string.Equals(temporaryCredential.Key, identifier, StringComparison.Ordinal), "Temporary credential identifiers do not match."); return await this.ProcessUserAuthorizationAsync(verifier, cancellationToken); } /// /// Obtains an access token after a successful user authorization. /// /// The verifier. /// The cancellation token. /// /// The access token assigned by the Service Provider. /// public async Task ProcessUserAuthorizationAsync(string verifier, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(verifier, "verifier"); Verify.Operation(this.ConsumerKey != null, Strings.RequiredPropertyNotYetPreset, "ConsumerKey"); Verify.Operation(this.TemporaryCredentialStorage != null, Strings.RequiredPropertyNotYetPreset, "TemporaryCredentialStorage"); Verify.Operation(this.ServiceProvider != null, Strings.RequiredPropertyNotYetPreset, "ServiceProvider"); var temporaryCredential = this.TemporaryCredentialStorage.RetrieveTemporaryCredential(); using (var client = this.CreateHttpClient(new AccessToken(temporaryCredential.Key, temporaryCredential.Value))) { var requestUri = new UriBuilder(this.ServiceProvider.TokenRequestEndpoint); requestUri.AppendQueryArgument(Protocol.VerifierParameter, verifier); var request = new HttpRequestMessage(this.ServiceProvider.TokenRequestEndpointMethod, requestUri.Uri); using (var response = await client.SendAsync(request, cancellationToken)) { response.EnsureSuccessStatusCode(); // Parse the response and ensure that it meets the requirements of the OAuth 1.0 spec. string content = await response.Content.ReadAsStringAsync(); var responseData = HttpUtility.ParseQueryString(content); string accessToken = responseData[Protocol.TokenParameter]; string tokenSecret = responseData[Protocol.TokenSecretParameter]; ErrorUtilities.VerifyProtocol(!string.IsNullOrEmpty(accessToken), MessagingStrings.RequiredParametersMissing, typeof(AuthorizedTokenResponse).Name, Protocol.TokenParameter); ErrorUtilities.VerifyProtocol(tokenSecret != null, MessagingStrings.RequiredParametersMissing, typeof(AuthorizedTokenResponse).Name, Protocol.TokenSecretParameter); responseData.Remove(Protocol.TokenParameter); responseData.Remove(Protocol.TokenSecretParameter); return new AccessTokenResponse(accessToken, tokenSecret, responseData); } } } /// /// Creates a message handler that signs outbound requests with a previously obtained authorization. /// /// The access token to authorize outbound HTTP requests with. /// The inner handler that actually sends the HTTP message on the network. /// /// A message handler. /// /// /// Overrides of this method may allow various derived types of handlers to be returned, /// enabling consumers that use RSA or other signing methods. /// public virtual OAuth1HttpMessageHandlerBase CreateMessageHandler(AccessToken accessToken = default(AccessToken), HttpMessageHandler innerHandler = null) { Verify.Operation(this.ConsumerKey != null, Strings.RequiredPropertyNotYetPreset, "ConsumerKey"); innerHandler = innerHandler ?? this.HostFactories.CreateHttpMessageHandler(); OAuth1HttpMessageHandlerBase handler; if (this.ConsumerCertificate != null) { handler = new OAuth1RsaSha1HttpMessageHandler(innerHandler) { SigningCertificate = this.ConsumerCertificate, }; } else { handler = new OAuth1HmacSha1HttpMessageHandler(innerHandler); } handler.ConsumerKey = this.ConsumerKey; handler.ConsumerSecret = this.ConsumerSecret; handler.AccessToken = accessToken.Token; handler.AccessTokenSecret = accessToken.Secret; return handler; } /// /// Creates the HTTP client. /// /// The access token to authorize outbound HTTP requests with. /// The inner handler that actually sends the HTTP message on the network. /// The HttpClient to use. public HttpClient CreateHttpClient(AccessToken accessToken, HttpMessageHandler innerHandler = null) { var handler = this.CreateMessageHandler(accessToken, innerHandler); var client = this.HostFactories.CreateHttpClient(handler); return client; } /// /// Creates the HTTP client. /// /// The inner handler that actually sends the HTTP message on the network. /// The HttpClient to use. public HttpClient CreateHttpClient(OAuth1HttpMessageHandlerBase innerHandler) { Requires.NotNull(innerHandler, "innerHandler"); var client = this.HostFactories.CreateHttpClient(innerHandler); return client; } } }