Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix-auth #2206

Merged
merged 2 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading