diff --git a/docs/docs/advanced/authenticators.md b/docs/docs/advanced/authenticators.md index 98c20a77e..14196d97c 100644 --- a/docs/docs/advanced/authenticators.md +++ b/docs/docs/advanced/authenticators.md @@ -22,7 +22,7 @@ var request = new RestRequest("/api/users/me") { var response = await client.ExecuteAsync(request, cancellationToken); ``` -## Basic Authentication +## Basic authentication The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string. @@ -36,43 +36,89 @@ var client = new RestClient(options); ## OAuth1 For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator. +OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature. + +The authenticator will use `HMAC SHA1` to create a signature by default. +Each static function to create the authenticator allows you to override the default and use another method to generate the signature. ### Request token +Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow. +Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator. This method requires a `consumerKey` and `consumerSecret` to authenticate. ```csharp -var options = new RestClientOptions("https://example.com") { +var options = new RestClientOptions("https://api.twitter.com") { Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret) }; var client = new RestClient(options); +var request = new RestRequest("oauth/request_token"); ``` +The response should contain the token and the token secret, which can then be used to complete the authorization process. +If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination. + ### Access token -This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +Getting an access token is the usual third step in the 3-legged OAuth1 flow. +This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`. +If you don't have a token for this call, you need to make a call to get the request token as described above. ```csharp var authenticator = OAuth1Authenticator.ForAccessToken( consumerKey, consumerSecret, oauthToken, oauthTokenSecret ); -var options = new RestClientOptions("https://example.com") { +var options = new RestClientOptions("https://api.twitter.com") { Authenticator = authenticator }; var client = new RestClient(options); +var request = new RestRequest("oauth/access_token"); +``` + +If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`: + +```csharp +var authenticator = OAuth1Authenticator.ForAccessToken( + consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier +); ``` -This method also includes an optional parameter to specify the `OAuthSignatureMethod`. +The response should contain the access token that can be used to make calls to protected resources. + +For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`. + +### Protected resource + +When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources. + ```csharp var authenticator = OAuth1Authenticator.ForAccessToken( - consumerKey, consumerSecret, oauthToken, oauthTokenSecret, - OAuthSignatureMethod.PlainText + consumerKey, consumerSecret, accessToken, accessTokenSecret +); +var options = new RestClientOptions("https://api.twitter.com/1.1") { + Authenticator = authenticator +}; +var client = new RestClient(options); +var request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); +``` + +### xAuth + +xAuth is a simplified version of OAuth1. It allows sending the username and password as `x_auth_username` and `x_auth_password` request parameters and directly get the access token. xAuth is not widely supported, but RestSharp still allows using it. + +Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function: + +```csharp +var authenticator = OAuth1Authenticator.ForClientAuthentication( + consumerKey, consumerSecret, username, password ); ``` ### 0-legged OAuth -The same access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. +The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`. ```csharp var authenticator = OAuth1Authenticator.ForAccessToken( @@ -120,7 +166,7 @@ For each request, it will add an `Authorization` header with the value `Bearer < As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token. -## Custom Authenticator +## Custom authenticator You can write your own implementation by implementing `IAuthenticator` and registering it with your RestClient: diff --git a/src/RestSharp/Authenticators/HttpBasicAuth.cs b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs similarity index 87% rename from src/RestSharp/Authenticators/HttpBasicAuth.cs rename to src/RestSharp/Authenticators/HttpBasicAuthenticator.cs index 89691c690..07d775664 100644 --- a/src/RestSharp/Authenticators/HttpBasicAuth.cs +++ b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs @@ -24,9 +24,9 @@ namespace RestSharp.Authenticators; /// UTF-8 is used by default but some servers might expect ISO-8859-1 encoding. /// [PublicAPI] -public class HttpBasicAuth(string username, string password, Encoding encoding) +public class HttpBasicAuthenticator(string username, string password, Encoding encoding) : AuthenticatorBase(GetHeader(username, password, encoding)) { - public HttpBasicAuth(string username, string password) : this(username, password, Encoding.UTF8) { } + public HttpBasicAuthenticator(string username, string password) : this(username, password, Encoding.UTF8) { } static string GetHeader(string username, string password, Encoding encoding) => Convert.ToBase64String(encoding.GetBytes($"{username}:{password}")); diff --git a/src/RestSharp/Authenticators/OAuth/OAuth1Auth.cs b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs similarity index 73% rename from src/RestSharp/Authenticators/OAuth/OAuth1Auth.cs rename to src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs index 81ee50389..4960bb12f 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuth1Auth.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs @@ -22,7 +22,7 @@ namespace RestSharp.Authenticators; /// RFC: The OAuth 1.0 Protocol -public class OAuth1Auth : IAuthenticator { +public class OAuth1Authenticator : IAuthenticator { public virtual string? Realm { get; set; } public virtual OAuthParameterHandling ParameterHandling { get; set; } public virtual OAuthSignatureMethod SignatureMethod { get; set; } @@ -56,12 +56,19 @@ public ValueTask Authenticate(IRestClient client, RestRequest request) { ClientPassword = ClientPassword }; - AddOAuthData(client, request, workflow); + AddOAuthData(client, request, workflow, Type, Realm); return default; } + /// + /// Creates an authenticator to retrieve a request token. + /// + /// Consumer or API key + /// Consumer or API secret + /// Signature method, default is HMAC SHA1 + /// Authenticator instance [PublicAPI] - public static OAuth1Auth ForRequestToken( + public static OAuth1Authenticator ForRequestToken( string consumerKey, string? consumerSecret, OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1 @@ -75,8 +82,15 @@ public static OAuth1Auth ForRequestToken( Type = OAuthType.RequestToken }; + /// + /// Creates an authenticator to retrieve a request token with custom callback. + /// + /// Consumer or API key + /// Consumer or API secret + /// URL to where the user will be redirected to after authhentication + /// Authenticator instance [PublicAPI] - public static OAuth1Auth ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) { + public static OAuth1Authenticator ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) { var authenticator = ForRequestToken(consumerKey, consumerSecret); authenticator.CallbackUrl = callbackUrl; @@ -84,8 +98,17 @@ public static OAuth1Auth ForRequestToken(string consumerKey, string? consumerSec return authenticator; } + /// + /// Creates an authenticator to retrieve an access token using the request token. + /// + /// Consumer or API key + /// Consumer or API secret + /// Request token + /// Request token secret + /// Signature method, default is HMAC SHA1 + /// Authenticator instance [PublicAPI] - public static OAuth1Auth ForAccessToken( + public static OAuth1Authenticator ForAccessToken( string consumerKey, string? consumerSecret, string token, @@ -103,8 +126,17 @@ public static OAuth1Auth ForAccessToken( Type = OAuthType.AccessToken }; + /// + /// Creates an authenticator to retrieve an access token using the request token and a verifier. + /// + /// Consumer or API key + /// Consumer or API secret + /// Request token + /// Request token secret + /// Verifier received from the API server + /// Authenticator instance [PublicAPI] - public static OAuth1Auth ForAccessToken( + public static OAuth1Authenticator ForAccessToken( string consumerKey, string? consumerSecret, string token, @@ -119,7 +151,7 @@ string verifier } [PublicAPI] - public static OAuth1Auth ForAccessTokenRefresh( + public static OAuth1Authenticator ForAccessTokenRefresh( string consumerKey, string? consumerSecret, string token, @@ -134,7 +166,7 @@ string sessionHandle } [PublicAPI] - public static OAuth1Auth ForAccessTokenRefresh( + public static OAuth1Authenticator ForAccessTokenRefresh( string consumerKey, string? consumerSecret, string token, @@ -151,7 +183,7 @@ string sessionHandle } [PublicAPI] - public static OAuth1Auth ForClientAuthentication( + public static OAuth1Authenticator ForClientAuthentication( string consumerKey, string? consumerSecret, string username, @@ -169,8 +201,17 @@ public static OAuth1Auth ForClientAuthentication( Type = OAuthType.ClientAuthentication }; + /// + /// Creates an authenticator to make calls to protected resources using the access token. + /// + /// Consumer or API key + /// Consumer or API secret + /// Access token + /// Access token secret + /// Signature method, default is HMAC SHA1 + /// Authenticator instance [PublicAPI] - public static OAuth1Auth ForProtectedResource( + public static OAuth1Authenticator ForProtectedResource( string consumerKey, string? consumerSecret, string accessToken, @@ -188,7 +229,13 @@ public static OAuth1Auth ForProtectedResource( TokenSecret = accessTokenSecret }; - void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflow) { + internal static void AddOAuthData( + IRestClient client, + RestRequest request, + OAuthWorkflow workflow, + OAuthType type, + string? realm + ) { var requestUrl = client.BuildUriWithoutQueryParameters(request).AbsoluteUri; if (requestUrl.Contains('?')) @@ -204,13 +251,6 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo var method = request.Method.ToString().ToUpperInvariant(); var parameters = new WebPairCollection(); - // include all GET and POST parameters before generating the signature - // according to the RFC 5849 - The OAuth 1.0 Protocol - // http://tools.ietf.org/html/rfc5849#section-3.4.1 - // if this change causes trouble we need to introduce a flag indicating the specific OAuth implementation level, - // or implement a separate class for each OAuth version - static bool BaseQuery(Parameter x) => x.Type is ParameterType.GetOrPost or ParameterType.QueryString; - var query = request.AlwaysMultipartFormData || request.Files.Count > 0 ? x => BaseQuery(x) && x.Name != null && x.Name.StartsWith("oauth_") @@ -219,22 +259,19 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo parameters.AddRange(client.DefaultParameters.Where(query).ToWebParameters()); parameters.AddRange(request.Parameters.Where(query).ToWebParameters()); - if (Type == OAuthType.RequestToken) - workflow.RequestTokenUrl = url; - else - workflow.AccessTokenUrl = url; + workflow.RequestUrl = url; - var oauth = Type switch { - OAuthType.RequestToken => workflow.BuildRequestTokenInfo(method, parameters), + var oauth = type switch { + OAuthType.RequestToken => workflow.BuildRequestTokenSignature(method, parameters), OAuthType.AccessToken => workflow.BuildAccessTokenSignature(method, parameters), OAuthType.ClientAuthentication => workflow.BuildClientAuthAccessTokenSignature(method, parameters), - OAuthType.ProtectedResource => workflow.BuildProtectedResourceSignature(method, parameters, url), + OAuthType.ProtectedResource => workflow.BuildProtectedResourceSignature(method, parameters), _ => throw new ArgumentOutOfRangeException(nameof(Type)) }; oauth.Parameters.Add("oauth_signature", oauth.Signature); - var oauthParameters = ParameterHandling switch { + var oauthParameters = workflow.ParameterHandling switch { OAuthParameterHandling.HttpAuthorizationHeader => CreateHeaderParameters(), OAuthParameterHandling.UrlOrPostParameters => CreateUrlParameters(), _ => throw new ArgumentOutOfRangeException(nameof(ParameterHandling)) @@ -243,7 +280,14 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo request.AddOrUpdateParameters(oauthParameters); return; - IEnumerable CreateHeaderParameters() => new[] { new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader()) }; + // include all GET and POST parameters before generating the signature + // according to the RFC 5849 - The OAuth 1.0 Protocol + // http://tools.ietf.org/html/rfc5849#section-3.4.1 + // if this change causes trouble we need to introduce a flag indicating the specific OAuth implementation level, + // or implement a separate class for each OAuth version + static bool BaseQuery(Parameter x) => x.Type is ParameterType.GetOrPost or ParameterType.QueryString; + + IEnumerable CreateHeaderParameters() => [new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader())]; IEnumerable CreateUrlParameters() => oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value))); @@ -254,7 +298,7 @@ string GetAuthorizationHeader() { .Select(x => x.GetQueryParameter(true)) .ToList(); - if (!Realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm)}\""); + if (!realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(realm)}\""); return $"OAuth {string.Join(",", oathParameters)}"; } diff --git a/src/RestSharp/Authenticators/OAuth/OAuthTools.cs b/src/RestSharp/Authenticators/OAuth/OAuthTools.cs index 1f54edafa..696440bd4 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuthTools.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuthTools.cs @@ -132,7 +132,7 @@ public static string GetNonce() { /// /// /// - static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters)); + internal static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters)); /// /// Sorts a by name, and then value if equal. @@ -193,24 +193,7 @@ public static string GetSignature( string signatureBase, string? consumerSecret ) - => GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret, null); - - /// - /// Creates a signature value given a signature base and the consumer secret. - /// This method is used when the token secret is currently unknown. - /// - /// The hashing method - /// The treatment to use on a signature value - /// The signature base - /// The consumer key - /// - public static string GetSignature( - OAuthSignatureMethod signatureMethod, - OAuthSignatureTreatment signatureTreatment, - string signatureBase, - string? consumerSecret - ) - => GetSignature(signatureMethod, signatureTreatment, signatureBase, consumerSecret, null); + => GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret); /// /// Creates a signature value given a signature base and the consumer secret and a known token secret. @@ -226,7 +209,7 @@ public static string GetSignature( OAuthSignatureTreatment signatureTreatment, string signatureBase, string? consumerSecret, - string? tokenSecret + string? tokenSecret = null ) { if (tokenSecret.IsEmpty()) tokenSecret = string.Empty; if (consumerSecret.IsEmpty()) consumerSecret = string.Empty; @@ -250,7 +233,8 @@ public static string GetSignature( return result; string GetRsaSignature() { - using var provider = new RSACryptoServiceProvider { PersistKeyInCsp = false }; + using var provider = new RSACryptoServiceProvider(); + provider.PersistKeyInCsp = false; provider.FromXmlString(unencodedConsumerSecret); diff --git a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs index 2039fb069..052325324 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs @@ -34,8 +34,10 @@ sealed class OAuthWorkflow { public OAuthParameterHandling ParameterHandling { get; set; } public string? ClientUsername { get; init; } public string? ClientPassword { get; init; } - public string? RequestTokenUrl { get; set; } - public string? AccessTokenUrl { get; set; } + public string? RequestUrl { get; set; } + + internal Func GetTimestamp { get; init; } = OAuthTools.GetTimestamp; + internal Func GetNonce { get; init; } = OAuthTools.GetNonce; /// /// Generates an OAuth signature to pass to an @@ -45,19 +47,20 @@ sealed class OAuthWorkflow { /// The HTTP method for the intended request /// Any existing, non-OAuth query parameters desired in the request /// - public OAuthParameters BuildRequestTokenInfo(string method, WebPairCollection parameters) { - ValidateTokenRequestState(); + public OAuthParameters BuildRequestTokenSignature(string method, WebPairCollection parameters) { + Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); var allParameters = new WebPairCollection(); allParameters.AddRange(parameters); - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); var authParameters = GenerateAuthParameters(timestamp, nonce); allParameters.AddRange(authParameters); - var signatureBase = OAuthTools.ConcatenateRequestElements(method, Ensure.NotNull(RequestTokenUrl, nameof(RequestTokenUrl)), allParameters); + var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), allParameters); return new OAuthParameters { Signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret), @@ -73,14 +76,15 @@ public OAuthParameters BuildRequestTokenInfo(string method, WebPairCollection pa /// The HTTP method for the intended request /// Any existing, non-OAuth query parameters desired in the request public OAuthParameters BuildAccessTokenSignature(string method, WebPairCollection parameters) { - ValidateAccessRequestState(); + Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); + Ensure.NotEmpty(Token, nameof(Token)); var allParameters = new WebPairCollection(); allParameters.AddRange(parameters); - var uri = new Uri(Ensure.NotEmptyString(AccessTokenUrl, nameof(AccessTokenUrl))); - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); var authParameters = GenerateAuthParameters(timestamp, nonce); allParameters.AddRange(authParameters); @@ -101,14 +105,15 @@ public OAuthParameters BuildAccessTokenSignature(string method, WebPairCollectio /// The HTTP method for the intended request /// Any existing, non-OAuth query parameters desired in the request public OAuthParameters BuildClientAuthAccessTokenSignature(string method, WebPairCollection parameters) { - ValidateClientAuthAccessRequestState(); + Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); + Ensure.NotEmpty(ClientUsername, nameof(ClientUsername)); var allParameters = new WebPairCollection(); allParameters.AddRange(parameters); - var uri = new Uri(Ensure.NotNull(AccessTokenUrl, nameof(AccessTokenUrl))); - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); var authParameters = GenerateXAuthParameters(timestamp, nonce); allParameters.AddRange(authParameters); @@ -121,25 +126,25 @@ public OAuthParameters BuildClientAuthAccessTokenSignature(string method, WebPai }; } - public OAuthParameters BuildProtectedResourceSignature(string method, WebPairCollection parameters, string url) { - ValidateProtectedResourceState(); + public OAuthParameters BuildProtectedResourceSignature(string method, WebPairCollection parameters) { + Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); var allParameters = new WebPairCollection(); allParameters.AddRange(parameters); // Include url parameters in query pool - var uri = new Uri(url); + var uri = new Uri(Ensure.NotEmptyString(RequestUrl, nameof(RequestUrl))); var urlParameters = HttpUtility.ParseQueryString(uri.Query); allParameters.AddRange(urlParameters.AllKeys.Select(x => new WebPair(x!, urlParameters[x]!))); - var timestamp = OAuthTools.GetTimestamp(); - var nonce = OAuthTools.GetNonce(); + var timestamp = GetTimestamp(); + var nonce = GetNonce(); var authParameters = GenerateAuthParameters(timestamp, nonce); allParameters.AddRange(authParameters); - var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, allParameters); + var signatureBase = OAuthTools.ConcatenateRequestElements(method, uri.ToString(), allParameters); return new OAuthParameters { Signature = OAuthTools.GetSignature(SignatureMethod, SignatureTreatment, signatureBase, ConsumerSecret, TokenSecret), @@ -147,25 +152,6 @@ public OAuthParameters BuildProtectedResourceSignature(string method, WebPairCol }; } - void ValidateTokenRequestState() { - Ensure.NotEmpty(RequestTokenUrl, nameof(RequestTokenUrl)); - Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); - } - - void ValidateAccessRequestState() { - Ensure.NotEmpty(AccessTokenUrl, nameof(AccessTokenUrl)); - Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); - Ensure.NotEmpty(Token, nameof(Token)); - } - - void ValidateClientAuthAccessRequestState() { - Ensure.NotEmpty(AccessTokenUrl, nameof(AccessTokenUrl)); - Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); - Ensure.NotEmpty(ClientUsername, nameof(ClientUsername)); - } - - void ValidateProtectedResourceState() => Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); - WebPairCollection GenerateAuthParameters(string timestamp, string nonce) => new WebPairCollection { new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey)), true), @@ -173,13 +159,14 @@ WebPairCollection GenerateAuthParameters(string timestamp, string nonce) new("oauth_signature_method", SignatureMethod.ToRequestValue()), new("oauth_timestamp", timestamp), new("oauth_version", Version ?? "1.0") - }.AddNotEmpty("oauth_token", Token!, true) - .AddNotEmpty("oauth_callback", CallbackUrl!, true) - .AddNotEmpty("oauth_verifier", Verifier!) - .AddNotEmpty("oauth_session_handle", SessionHandle!); + } + .AddNotEmpty("oauth_token", Token, true) + .AddNotEmpty("oauth_callback", CallbackUrl, true) + .AddNotEmpty("oauth_verifier", Verifier) + .AddNotEmpty("oauth_session_handle", SessionHandle); WebPairCollection GenerateXAuthParameters(string timestamp, string nonce) - => new() { + => [ new("x_auth_username", Ensure.NotNull(ClientUsername, nameof(ClientUsername))), new("x_auth_password", Ensure.NotNull(ClientPassword, nameof(ClientPassword))), new("x_auth_mode", "client_auth"), @@ -188,7 +175,7 @@ WebPairCollection GenerateXAuthParameters(string timestamp, string nonce) new("oauth_timestamp", timestamp), new("oauth_nonce", nonce), new("oauth_version", Version ?? "1.0") - }; + ]; internal class OAuthParameters { public WebPairCollection Parameters { get; init; } = null!; diff --git a/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs b/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs index 59f9a71a0..55a6bf5d5 100644 --- a/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs +++ b/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs @@ -24,18 +24,12 @@ namespace RestSharp.Authenticators.OAuth2; public class OAuth2AuthorizationRequestHeaderAuthenticator : AuthenticatorBase { readonly string _tokenType; - /// - /// Initializes a new instance of the class. - /// - /// The access token. - public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken) : this(accessToken, "OAuth") { } - /// /// Initializes a new instance of the class. /// /// The access token. /// The token type. - public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken, string tokenType) : base(accessToken) => _tokenType = tokenType; + public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken, string tokenType = "OAuth") : base(accessToken) => _tokenType = tokenType; protected override ValueTask GetAuthenticationParameter(string accessToken) => new(new HeaderParameter(KnownHeaders.Authorization, $"{_tokenType} {accessToken}")); diff --git a/src/RestSharp/Extensions/CookieContainerExtensions.cs b/src/RestSharp/Extensions/CookieContainerExtensions.cs index 519a497d3..8df9142d7 100644 --- a/src/RestSharp/Extensions/CookieContainerExtensions.cs +++ b/src/RestSharp/Extensions/CookieContainerExtensions.cs @@ -13,8 +13,6 @@ // limitations under the License. // -using System.Net; - namespace RestSharp.Extensions; static class CookieContainerExtensions { diff --git a/src/RestSharp/KnownHeaders.cs b/src/RestSharp/KnownHeaders.cs index 2cd925710..3871c4d59 100644 --- a/src/RestSharp/KnownHeaders.cs +++ b/src/RestSharp/KnownHeaders.cs @@ -47,4 +47,4 @@ public static class KnownHeaders { static readonly HashSet ContentHeadersHash = new(ContentHeaders, StringComparer.InvariantCultureIgnoreCase); internal static bool IsContentHeader(string key) => ContentHeadersHash.Contains(key); -} +} \ No newline at end of file diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index ba28ff3d0..55e02c70c 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -12,15 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System.Net; using RestSharp.Extensions; -using RestSharp.Interceptors; namespace RestSharp; public partial class RestClient { // Default HttpClient timeout - public TimeSpan DefaultTimeout = TimeSpan.FromSeconds(100); + readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(100); + /// public async Task ExecuteAsync(RestRequest request, CancellationToken cancellationToken = default) { using var internalResponse = await ExecuteRequestAsync(request, cancellationToken).ConfigureAwait(false); @@ -115,7 +114,7 @@ async Task ExecuteRequestAsync(RestRequest request, CancellationTo message.Headers.Host = Options.BaseHost; message.Headers.CacheControl = request.CachePolicy ?? Options.CachePolicy; - using var timeoutCts = new CancellationTokenSource(request.Timeout ?? Options.Timeout ?? DefaultTimeout); + using var timeoutCts = new CancellationTokenSource(request.Timeout ?? Options.Timeout ?? _defaultTimeout); using var cts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken); var ct = cts.Token; diff --git a/test/RestSharp.InteractiveTests/AuthenticationTests.cs b/test/RestSharp.InteractiveTests/AuthenticationTests.cs index 8c0aea8c0..4930d6ca0 100644 --- a/test/RestSharp.InteractiveTests/AuthenticationTests.cs +++ b/test/RestSharp.InteractiveTests/AuthenticationTests.cs @@ -18,7 +18,7 @@ public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(Twitter using var client = new RestClient( baseUrl, options => - options.Authenticator = OAuth1Auth.ForRequestToken( + options.Authenticator = OAuth1Authenticator.ForRequestToken( twitterKeys.ConsumerKey!, twitterKeys.ConsumerSecret, "https://restsharp.dev" @@ -47,7 +47,7 @@ public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(Twitter var verifier = Console.ReadLine(); request = new RestRequest("oauth/access_token") { - Authenticator = OAuth1Auth.ForAccessToken( + Authenticator = OAuth1Authenticator.ForAccessToken( twitterKeys.ConsumerKey!, twitterKeys.ConsumerSecret, oauthToken!, @@ -69,7 +69,7 @@ public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(Twitter Assert.NotNull(oauthTokenSecret); request = new RestRequest("1.1/account/verify_credentials.json") { - Authenticator = OAuth1Auth.ForProtectedResource( + Authenticator = OAuth1Authenticator.ForProtectedResource( twitterKeys.ConsumerKey!, twitterKeys.ConsumerSecret, oauthToken!, diff --git a/test/RestSharp.InteractiveTests/TwitterClient.cs b/test/RestSharp.InteractiveTests/TwitterClient.cs index 6324acf26..24b95e8c7 100644 --- a/test/RestSharp.InteractiveTests/TwitterClient.cs +++ b/test/RestSharp.InteractiveTests/TwitterClient.cs @@ -93,7 +93,7 @@ protected override async ValueTask GetAuthenticationParameter(string async Task GetToken() { var options = new RestClientOptions(_baseUrl) { - Authenticator = new HttpBasicAuth(_clientId, _clientSecret) + Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret) }; using var client = new RestClient(options); diff --git a/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs b/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs index 249818c00..6a8a43df2 100644 --- a/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs +++ b/test/RestSharp.Tests.Integrated/Authentication/AuthenticationTests.cs @@ -12,7 +12,7 @@ public async Task Can_Authenticate_With_Basic_Http_Auth() { using var client = new RestClient( server.Url!, - o => o.Authenticator = new HttpBasicAuth(userName, password) + o => o.Authenticator = new HttpBasicAuthenticator(userName, password) ); var request = new RestRequest("headers"); var response = await client.GetAsync(request); diff --git a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs index 981b552ee..fd2f6905f 100644 --- a/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs +++ b/test/RestSharp.Tests.Integrated/NonProtocolExceptionHandlingTests.cs @@ -45,7 +45,7 @@ public async Task Handles_HttpClient_Timeout_Error() { public async Task Handles_Server_Timeout_Error() { using var client = new RestClient(_server.Url!); - var request = new RestRequest("404") { Timeout = TimeSpan.FromMilliseconds(500) }; + var request = new RestRequest("timeout") { Timeout = TimeSpan.FromMilliseconds(500) }; var response = await client.ExecuteAsync(request); response.ErrorException.Should().BeOfType(); diff --git a/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs b/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs index fd2d16746..a8f3ea3ef 100644 --- a/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs +++ b/test/RestSharp.Tests/Auth/HttpBasicAuthTests.cs @@ -7,7 +7,7 @@ public class HttpBasicAuthTests { const string Username = "username"; const string Password = "password"; - readonly HttpBasicAuth _auth = new(Username, Password); + readonly HttpBasicAuthenticator _auth = new(Username, Password); [Fact] public async Task Authenticate_ShouldAddAuthorizationParameter_IfPreviouslyUnassigned() { diff --git a/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs b/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs index 36b1b0539..cbbcb0401 100644 --- a/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs +++ b/test/RestSharp.Tests/Auth/OAuth1AuthTests.cs @@ -4,7 +4,7 @@ namespace RestSharp.Tests.Auth; public class OAuth1AuthTests { - readonly OAuth1Auth _auth = new() { + readonly OAuth1Authenticator _auth = new() { CallbackUrl = "CallbackUrl", ClientPassword = "ClientPassword", Type = OAuthType.ClientAuthentication, diff --git a/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs b/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs new file mode 100644 index 000000000..b489b3dc6 --- /dev/null +++ b/test/RestSharp.Tests/Auth/OAuth1SignatureTests.cs @@ -0,0 +1,52 @@ +using RestSharp.Authenticators; +using RestSharp.Authenticators.OAuth; + +namespace RestSharp.Tests.Auth; + +public class OAuth1SignatureTests { + readonly OAuthWorkflow _workflow = new() { + ParameterHandling = OAuthParameterHandling.UrlOrPostParameters, + Token = "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", + TokenSecret = "LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE", + ConsumerKey = "xvz1evFS4wEEPTGEFPHBog", + ConsumerSecret = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw", + SignatureMethod = OAuthSignatureMethod.HmacSha1, + Version = "1.0", + GetNonce = () => "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", + GetTimestamp = () => "1318622958", + }; + + readonly RestClient _client = new("https://api.twitter.com/1.1"); + + readonly RestRequest _request = new RestRequest("statuses/update.json", Method.Post) + .AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!") + .AddParameter("include_entities", "true"); + + [Fact] + public void Adds_correct_signature() { + OAuth1Authenticator.AddOAuthData(_client, _request, _workflow, OAuthType.ProtectedResource, null); + + var signature = _request.Parameters.First(x => x.Name == "oauth_signature").Value; + signature.Should().Be("hCtSmYh+iHYCEqBWrE7C7hYmtUk="); + } + + [Fact] + public void Generates_correct_signature_base() { + const string method = "POST"; + + var requestParameters = _request.Parameters.ToWebParameters().ToArray(); + var parameters = new WebPairCollection(); + parameters.AddRange(requestParameters); + var url = _client.BuildUri(_request).ToString(); + _workflow.RequestUrl = url; + var oauthParameters = _workflow.BuildProtectedResourceSignature(method, parameters); + oauthParameters.Parameters.AddRange(requestParameters); + + var signatureBase = OAuthTools.ConcatenateRequestElements(method, url, oauthParameters.Parameters); + + signatureBase.Should() + .Be( + "POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521" + ); + } +} \ No newline at end of file diff --git a/test/RestSharp.Tests/Auth/OAuth1Tests.cs b/test/RestSharp.Tests/Auth/OAuth1Tests.cs index 14b76c168..483a89771 100644 --- a/test/RestSharp.Tests/Auth/OAuth1Tests.cs +++ b/test/RestSharp.Tests/Auth/OAuth1Tests.cs @@ -42,7 +42,7 @@ public async Task Can_Authenticate_OAuth1_With_Querystring_Parameters() { using var client = new RestClient(baseUrl); var request = new RestRequest(); - var authenticator = OAuth1Auth.ForRequestToken(consumerKey, consumerSecret); + var authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret); authenticator.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters; await authenticator.Authenticate(client, request); @@ -74,22 +74,14 @@ public void Properly_Encodes_Parameter_Names(IList<(string, string)> parameters, }; [Fact] - public void Use_RFC_3986_Encoding_For_Auth_Signature_Base() { - // reserved characters for 2396 and 3986 - // http://www.ietf.org/rfc/rfc2396.txt - string[] reserved2396Characters = { ";", "/", "?", ":", "@", "&", "=", "+", "$", "," }; - // http://www.ietf.org/rfc/rfc3986.txt - string[] additionalReserved3986Characters = { "!", "*", "'", "(", ")" }; - - var reservedCharacterString = string.Join( - string.Empty, - reserved2396Characters.Union(additionalReserved3986Characters) - ); - - // act - var escapedString = OAuthTools.UrlEncodeRelaxed(reservedCharacterString); - - // assert - Assert.Equal("%3B%2F%3F%3A%40%26%3D%2B%24%2C%2521%252A%2527%2528%2529", escapedString); + public void Encodes_parameter() { + var parameter = new WebPair("status", "Hello Ladies + Gentlemen, a signed OAuth request!"); + var parameters = new WebPairCollection { parameter }; + + const string expected = "status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521"; + + var norm = OAuthTools.NormalizeRequestParameters(parameters); + var escaped = OAuthTools.UrlEncodeRelaxed(norm); + escaped.Should().Be(expected); } } \ No newline at end of file