Skip to content

Commit

Permalink
fix-auth (#2206)
Browse files Browse the repository at this point in the history
* Add tests to OAuth1 signature and revert accidental authenticator renames

* Fix wrong timeout endpoint
  • Loading branch information
alexeyzimarev authored May 26, 2024
1 parent 33241b0 commit fd392c9
Show file tree
Hide file tree
Showing 17 changed files with 242 additions and 146 deletions.
64 changes: 55 additions & 9 deletions docs/docs/advanced/authenticators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ namespace RestSharp.Authenticators;
/// UTF-8 is used by default but some servers might expect ISO-8859-1 encoding.
/// </remarks>
[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}"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
namespace RestSharp.Authenticators;

/// <seealso href="http://tools.ietf.org/html/rfc5849">RFC: The OAuth 1.0 Protocol</seealso>
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; }
Expand Down Expand Up @@ -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;
}

/// <summary>
/// Creates an authenticator to retrieve a request token.
/// </summary>
/// <param name="consumerKey">Consumer or API key</param>
/// <param name="consumerSecret">Consumer or API secret</param>
/// <param name="signatureMethod">Signature method, default is HMAC SHA1</param>
/// <returns>Authenticator instance</returns>
[PublicAPI]
public static OAuth1Auth ForRequestToken(
public static OAuth1Authenticator ForRequestToken(
string consumerKey,
string? consumerSecret,
OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1
Expand All @@ -75,17 +82,33 @@ public static OAuth1Auth ForRequestToken(
Type = OAuthType.RequestToken
};

/// <summary>
/// Creates an authenticator to retrieve a request token with custom callback.
/// </summary>
/// <param name="consumerKey">Consumer or API key</param>
/// <param name="consumerSecret">Consumer or API secret</param>
/// <param name="callbackUrl">URL to where the user will be redirected to after authhentication</param>
/// <returns>Authenticator instance</returns>
[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;

return authenticator;
}

/// <summary>
/// Creates an authenticator to retrieve an access token using the request token.
/// </summary>
/// <param name="consumerKey">Consumer or API key</param>
/// <param name="consumerSecret">Consumer or API secret</param>
/// <param name="token">Request token</param>
/// <param name="tokenSecret">Request token secret</param>
/// <param name="signatureMethod">Signature method, default is HMAC SHA1</param>
/// <returns>Authenticator instance</returns>
[PublicAPI]
public static OAuth1Auth ForAccessToken(
public static OAuth1Authenticator ForAccessToken(
string consumerKey,
string? consumerSecret,
string token,
Expand All @@ -103,8 +126,17 @@ public static OAuth1Auth ForAccessToken(
Type = OAuthType.AccessToken
};

/// <summary>
/// Creates an authenticator to retrieve an access token using the request token and a verifier.
/// </summary>
/// <param name="consumerKey">Consumer or API key</param>
/// <param name="consumerSecret">Consumer or API secret</param>
/// <param name="token">Request token</param>
/// <param name="tokenSecret">Request token secret</param>
/// <param name="verifier">Verifier received from the API server</param>
/// <returns>Authenticator instance</returns>
[PublicAPI]
public static OAuth1Auth ForAccessToken(
public static OAuth1Authenticator ForAccessToken(
string consumerKey,
string? consumerSecret,
string token,
Expand All @@ -119,7 +151,7 @@ string verifier
}

[PublicAPI]
public static OAuth1Auth ForAccessTokenRefresh(
public static OAuth1Authenticator ForAccessTokenRefresh(
string consumerKey,
string? consumerSecret,
string token,
Expand All @@ -134,7 +166,7 @@ string sessionHandle
}

[PublicAPI]
public static OAuth1Auth ForAccessTokenRefresh(
public static OAuth1Authenticator ForAccessTokenRefresh(
string consumerKey,
string? consumerSecret,
string token,
Expand All @@ -151,7 +183,7 @@ string sessionHandle
}

[PublicAPI]
public static OAuth1Auth ForClientAuthentication(
public static OAuth1Authenticator ForClientAuthentication(
string consumerKey,
string? consumerSecret,
string username,
Expand All @@ -169,8 +201,17 @@ public static OAuth1Auth ForClientAuthentication(
Type = OAuthType.ClientAuthentication
};

/// <summary>
/// Creates an authenticator to make calls to protected resources using the access token.
/// </summary>
/// <param name="consumerKey">Consumer or API key</param>
/// <param name="consumerSecret">Consumer or API secret</param>
/// <param name="accessToken">Access token</param>
/// <param name="accessTokenSecret">Access token secret</param>
/// <param name="signatureMethod">Signature method, default is HMAC SHA1</param>
/// <returns>Authenticator instance</returns>
[PublicAPI]
public static OAuth1Auth ForProtectedResource(
public static OAuth1Authenticator ForProtectedResource(
string consumerKey,
string? consumerSecret,
string accessToken,
Expand All @@ -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('?'))
Expand All @@ -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_")
Expand All @@ -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))
Expand All @@ -243,7 +280,14 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo
request.AddOrUpdateParameters(oauthParameters);
return;

IEnumerable<Parameter> 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<Parameter> CreateHeaderParameters() => [new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader())];

IEnumerable<Parameter> CreateUrlParameters() => oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value)));

Expand All @@ -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)}";
}
Expand Down
26 changes: 5 additions & 21 deletions src/RestSharp/Authenticators/OAuth/OAuthTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public static string GetNonce() {
/// </summary>
/// <param name="parameters"></param>
/// <returns></returns>
static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters));
internal static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters));

/// <summary>
/// Sorts a <see cref="WebPairCollection" /> by name, and then value if equal.
Expand Down Expand Up @@ -193,24 +193,7 @@ public static string GetSignature(
string signatureBase,
string? consumerSecret
)
=> GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret, null);

/// <summary>
/// Creates a signature value given a signature base and the consumer secret.
/// This method is used when the token secret is currently unknown.
/// </summary>
/// <param name="signatureMethod">The hashing method</param>
/// <param name="signatureTreatment">The treatment to use on a signature value</param>
/// <param name="signatureBase">The signature base</param>
/// <param name="consumerSecret">The consumer key</param>
/// <returns></returns>
public static string GetSignature(
OAuthSignatureMethod signatureMethod,
OAuthSignatureTreatment signatureTreatment,
string signatureBase,
string? consumerSecret
)
=> GetSignature(signatureMethod, signatureTreatment, signatureBase, consumerSecret, null);
=> GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret);

/// <summary>
/// Creates a signature value given a signature base and the consumer secret and a known token secret.
Expand All @@ -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;
Expand All @@ -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);

Expand Down
Loading

0 comments on commit fd392c9

Please sign in to comment.