Skip to content

Commit fd392c9

Browse files
fix-auth (#2206)
* Add tests to OAuth1 signature and revert accidental authenticator renames * Fix wrong timeout endpoint
1 parent 33241b0 commit fd392c9

File tree

17 files changed

+242
-146
lines changed

17 files changed

+242
-146
lines changed

docs/docs/advanced/authenticators.md

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var request = new RestRequest("/api/users/me") {
2222
var response = await client.ExecuteAsync(request, cancellationToken);
2323
```
2424

25-
## Basic Authentication
25+
## Basic authentication
2626

2727
The `HttpBasicAuthenticator` allows you pass a username and password as a basic `Authorization` header using a base64 encoded string.
2828

@@ -36,43 +36,89 @@ var client = new RestClient(options);
3636
## OAuth1
3737

3838
For OAuth1 authentication the `OAuth1Authenticator` class provides static methods to help generate an OAuth authenticator.
39+
OAuth1 authenticator will add the necessary OAuth parameters to the request, including signature.
40+
41+
The authenticator will use `HMAC SHA1` to create a signature by default.
42+
Each static function to create the authenticator allows you to override the default and use another method to generate the signature.
3943

4044
### Request token
4145

46+
Getting a temporary request token is the usual first step in the 3-legged OAuth1 flow.
47+
Use `OAuth1Authenticator.ForRequestToken` function to get the request token authenticator.
4248
This method requires a `consumerKey` and `consumerSecret` to authenticate.
4349

4450
```csharp
45-
var options = new RestClientOptions("https://example.com") {
51+
var options = new RestClientOptions("https://api.twitter.com") {
4652
Authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret)
4753
};
4854
var client = new RestClient(options);
55+
var request = new RestRequest("oauth/request_token");
4956
```
5057

58+
The response should contain the token and the token secret, which can then be used to complete the authorization process.
59+
If you need to provide the callback URL, assign the `CallbackUrl` property of the authenticator to the callback destination.
60+
5161
### Access token
5262

53-
This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`.
63+
Getting an access token is the usual third step in the 3-legged OAuth1 flow.
64+
This method retrieves an access token when provided `consumerKey`, `consumerSecret`, `oauthToken`, and `oauthTokenSecret`.
65+
If you don't have a token for this call, you need to make a call to get the request token as described above.
5466

5567
```csharp
5668
var authenticator = OAuth1Authenticator.ForAccessToken(
5769
consumerKey, consumerSecret, oauthToken, oauthTokenSecret
5870
);
59-
var options = new RestClientOptions("https://example.com") {
71+
var options = new RestClientOptions("https://api.twitter.com") {
6072
Authenticator = authenticator
6173
};
6274
var client = new RestClient(options);
75+
var request = new RestRequest("oauth/access_token");
76+
```
77+
78+
If the second step in 3-leg OAuth1 flow returned a verifier value, you can use another overload of `ForAccessToken`:
79+
80+
```csharp
81+
var authenticator = OAuth1Authenticator.ForAccessToken(
82+
consumerKey, consumerSecret, oauthToken, oauthTokenSecret, verifier
83+
);
6384
```
6485

65-
This method also includes an optional parameter to specify the `OAuthSignatureMethod`.
86+
The response should contain the access token that can be used to make calls to protected resources.
87+
88+
For refreshing access tokens, use one of the two overloads of `ForAccessToken` that accept `sessionHandle`.
89+
90+
### Protected resource
91+
92+
When the access token is available, use `ForProtectedResource` function to get the authenticator for accessing protected resources.
93+
6694
```csharp
6795
var authenticator = OAuth1Authenticator.ForAccessToken(
68-
consumerKey, consumerSecret, oauthToken, oauthTokenSecret,
69-
OAuthSignatureMethod.PlainText
96+
consumerKey, consumerSecret, accessToken, accessTokenSecret
97+
);
98+
var options = new RestClientOptions("https://api.twitter.com/1.1") {
99+
Authenticator = authenticator
100+
};
101+
var client = new RestClient(options);
102+
var request = new RestRequest("statuses/update.json", Method.Post)
103+
.AddParameter("status", "Hello Ladies + Gentlemen, a signed OAuth request!")
104+
.AddParameter("include_entities", "true");
105+
```
106+
107+
### xAuth
108+
109+
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.
110+
111+
Create an xAuth authenticator using `OAuth1Authenticator.ForClientAuthentication` function:
112+
113+
```csharp
114+
var authenticator = OAuth1Authenticator.ForClientAuthentication(
115+
consumerKey, consumerSecret, username, password
70116
);
71117
```
72118

73119
### 0-legged OAuth
74120

75-
The same access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`.
121+
The access token authenticator can be used in 0-legged OAuth scenarios by providing `null` for the `consumerSecret`.
76122

77123
```csharp
78124
var authenticator = OAuth1Authenticator.ForAccessToken(
@@ -120,7 +166,7 @@ For each request, it will add an `Authorization` header with the value `Bearer <
120166

121167
As you might need to refresh the token from, you can use the `SetBearerToken` method to update the token.
122168

123-
## Custom Authenticator
169+
## Custom authenticator
124170

125171
You can write your own implementation by implementing `IAuthenticator` and
126172
registering it with your RestClient:

src/RestSharp/Authenticators/HttpBasicAuth.cs renamed to src/RestSharp/Authenticators/HttpBasicAuthenticator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ namespace RestSharp.Authenticators;
2424
/// UTF-8 is used by default but some servers might expect ISO-8859-1 encoding.
2525
/// </remarks>
2626
[PublicAPI]
27-
public class HttpBasicAuth(string username, string password, Encoding encoding)
27+
public class HttpBasicAuthenticator(string username, string password, Encoding encoding)
2828
: AuthenticatorBase(GetHeader(username, password, encoding)) {
29-
public HttpBasicAuth(string username, string password) : this(username, password, Encoding.UTF8) { }
29+
public HttpBasicAuthenticator(string username, string password) : this(username, password, Encoding.UTF8) { }
3030

3131
static string GetHeader(string username, string password, Encoding encoding)
3232
=> Convert.ToBase64String(encoding.GetBytes($"{username}:{password}"));

src/RestSharp/Authenticators/OAuth/OAuth1Auth.cs renamed to src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
namespace RestSharp.Authenticators;
2323

2424
/// <seealso href="http://tools.ietf.org/html/rfc5849">RFC: The OAuth 1.0 Protocol</seealso>
25-
public class OAuth1Auth : IAuthenticator {
25+
public class OAuth1Authenticator : IAuthenticator {
2626
public virtual string? Realm { get; set; }
2727
public virtual OAuthParameterHandling ParameterHandling { get; set; }
2828
public virtual OAuthSignatureMethod SignatureMethod { get; set; }
@@ -56,12 +56,19 @@ public ValueTask Authenticate(IRestClient client, RestRequest request) {
5656
ClientPassword = ClientPassword
5757
};
5858

59-
AddOAuthData(client, request, workflow);
59+
AddOAuthData(client, request, workflow, Type, Realm);
6060
return default;
6161
}
6262

63+
/// <summary>
64+
/// Creates an authenticator to retrieve a request token.
65+
/// </summary>
66+
/// <param name="consumerKey">Consumer or API key</param>
67+
/// <param name="consumerSecret">Consumer or API secret</param>
68+
/// <param name="signatureMethod">Signature method, default is HMAC SHA1</param>
69+
/// <returns>Authenticator instance</returns>
6370
[PublicAPI]
64-
public static OAuth1Auth ForRequestToken(
71+
public static OAuth1Authenticator ForRequestToken(
6572
string consumerKey,
6673
string? consumerSecret,
6774
OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1
@@ -75,17 +82,33 @@ public static OAuth1Auth ForRequestToken(
7582
Type = OAuthType.RequestToken
7683
};
7784

85+
/// <summary>
86+
/// Creates an authenticator to retrieve a request token with custom callback.
87+
/// </summary>
88+
/// <param name="consumerKey">Consumer or API key</param>
89+
/// <param name="consumerSecret">Consumer or API secret</param>
90+
/// <param name="callbackUrl">URL to where the user will be redirected to after authhentication</param>
91+
/// <returns>Authenticator instance</returns>
7892
[PublicAPI]
79-
public static OAuth1Auth ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) {
93+
public static OAuth1Authenticator ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) {
8094
var authenticator = ForRequestToken(consumerKey, consumerSecret);
8195

8296
authenticator.CallbackUrl = callbackUrl;
8397

8498
return authenticator;
8599
}
86100

101+
/// <summary>
102+
/// Creates an authenticator to retrieve an access token using the request token.
103+
/// </summary>
104+
/// <param name="consumerKey">Consumer or API key</param>
105+
/// <param name="consumerSecret">Consumer or API secret</param>
106+
/// <param name="token">Request token</param>
107+
/// <param name="tokenSecret">Request token secret</param>
108+
/// <param name="signatureMethod">Signature method, default is HMAC SHA1</param>
109+
/// <returns>Authenticator instance</returns>
87110
[PublicAPI]
88-
public static OAuth1Auth ForAccessToken(
111+
public static OAuth1Authenticator ForAccessToken(
89112
string consumerKey,
90113
string? consumerSecret,
91114
string token,
@@ -103,8 +126,17 @@ public static OAuth1Auth ForAccessToken(
103126
Type = OAuthType.AccessToken
104127
};
105128

129+
/// <summary>
130+
/// Creates an authenticator to retrieve an access token using the request token and a verifier.
131+
/// </summary>
132+
/// <param name="consumerKey">Consumer or API key</param>
133+
/// <param name="consumerSecret">Consumer or API secret</param>
134+
/// <param name="token">Request token</param>
135+
/// <param name="tokenSecret">Request token secret</param>
136+
/// <param name="verifier">Verifier received from the API server</param>
137+
/// <returns>Authenticator instance</returns>
106138
[PublicAPI]
107-
public static OAuth1Auth ForAccessToken(
139+
public static OAuth1Authenticator ForAccessToken(
108140
string consumerKey,
109141
string? consumerSecret,
110142
string token,
@@ -119,7 +151,7 @@ string verifier
119151
}
120152

121153
[PublicAPI]
122-
public static OAuth1Auth ForAccessTokenRefresh(
154+
public static OAuth1Authenticator ForAccessTokenRefresh(
123155
string consumerKey,
124156
string? consumerSecret,
125157
string token,
@@ -134,7 +166,7 @@ string sessionHandle
134166
}
135167

136168
[PublicAPI]
137-
public static OAuth1Auth ForAccessTokenRefresh(
169+
public static OAuth1Authenticator ForAccessTokenRefresh(
138170
string consumerKey,
139171
string? consumerSecret,
140172
string token,
@@ -151,7 +183,7 @@ string sessionHandle
151183
}
152184

153185
[PublicAPI]
154-
public static OAuth1Auth ForClientAuthentication(
186+
public static OAuth1Authenticator ForClientAuthentication(
155187
string consumerKey,
156188
string? consumerSecret,
157189
string username,
@@ -169,8 +201,17 @@ public static OAuth1Auth ForClientAuthentication(
169201
Type = OAuthType.ClientAuthentication
170202
};
171203

204+
/// <summary>
205+
/// Creates an authenticator to make calls to protected resources using the access token.
206+
/// </summary>
207+
/// <param name="consumerKey">Consumer or API key</param>
208+
/// <param name="consumerSecret">Consumer or API secret</param>
209+
/// <param name="accessToken">Access token</param>
210+
/// <param name="accessTokenSecret">Access token secret</param>
211+
/// <param name="signatureMethod">Signature method, default is HMAC SHA1</param>
212+
/// <returns>Authenticator instance</returns>
172213
[PublicAPI]
173-
public static OAuth1Auth ForProtectedResource(
214+
public static OAuth1Authenticator ForProtectedResource(
174215
string consumerKey,
175216
string? consumerSecret,
176217
string accessToken,
@@ -188,7 +229,13 @@ public static OAuth1Auth ForProtectedResource(
188229
TokenSecret = accessTokenSecret
189230
};
190231

191-
void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflow) {
232+
internal static void AddOAuthData(
233+
IRestClient client,
234+
RestRequest request,
235+
OAuthWorkflow workflow,
236+
OAuthType type,
237+
string? realm
238+
) {
192239
var requestUrl = client.BuildUriWithoutQueryParameters(request).AbsoluteUri;
193240

194241
if (requestUrl.Contains('?'))
@@ -204,13 +251,6 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo
204251
var method = request.Method.ToString().ToUpperInvariant();
205252
var parameters = new WebPairCollection();
206253

207-
// include all GET and POST parameters before generating the signature
208-
// according to the RFC 5849 - The OAuth 1.0 Protocol
209-
// http://tools.ietf.org/html/rfc5849#section-3.4.1
210-
// if this change causes trouble we need to introduce a flag indicating the specific OAuth implementation level,
211-
// or implement a separate class for each OAuth version
212-
static bool BaseQuery(Parameter x) => x.Type is ParameterType.GetOrPost or ParameterType.QueryString;
213-
214254
var query =
215255
request.AlwaysMultipartFormData || request.Files.Count > 0
216256
? x => BaseQuery(x) && x.Name != null && x.Name.StartsWith("oauth_")
@@ -219,22 +259,19 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo
219259
parameters.AddRange(client.DefaultParameters.Where(query).ToWebParameters());
220260
parameters.AddRange(request.Parameters.Where(query).ToWebParameters());
221261

222-
if (Type == OAuthType.RequestToken)
223-
workflow.RequestTokenUrl = url;
224-
else
225-
workflow.AccessTokenUrl = url;
262+
workflow.RequestUrl = url;
226263

227-
var oauth = Type switch {
228-
OAuthType.RequestToken => workflow.BuildRequestTokenInfo(method, parameters),
264+
var oauth = type switch {
265+
OAuthType.RequestToken => workflow.BuildRequestTokenSignature(method, parameters),
229266
OAuthType.AccessToken => workflow.BuildAccessTokenSignature(method, parameters),
230267
OAuthType.ClientAuthentication => workflow.BuildClientAuthAccessTokenSignature(method, parameters),
231-
OAuthType.ProtectedResource => workflow.BuildProtectedResourceSignature(method, parameters, url),
268+
OAuthType.ProtectedResource => workflow.BuildProtectedResourceSignature(method, parameters),
232269
_ => throw new ArgumentOutOfRangeException(nameof(Type))
233270
};
234271

235272
oauth.Parameters.Add("oauth_signature", oauth.Signature);
236273

237-
var oauthParameters = ParameterHandling switch {
274+
var oauthParameters = workflow.ParameterHandling switch {
238275
OAuthParameterHandling.HttpAuthorizationHeader => CreateHeaderParameters(),
239276
OAuthParameterHandling.UrlOrPostParameters => CreateUrlParameters(),
240277
_ => throw new ArgumentOutOfRangeException(nameof(ParameterHandling))
@@ -243,7 +280,14 @@ void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflo
243280
request.AddOrUpdateParameters(oauthParameters);
244281
return;
245282

246-
IEnumerable<Parameter> CreateHeaderParameters() => new[] { new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader()) };
283+
// include all GET and POST parameters before generating the signature
284+
// according to the RFC 5849 - The OAuth 1.0 Protocol
285+
// http://tools.ietf.org/html/rfc5849#section-3.4.1
286+
// if this change causes trouble we need to introduce a flag indicating the specific OAuth implementation level,
287+
// or implement a separate class for each OAuth version
288+
static bool BaseQuery(Parameter x) => x.Type is ParameterType.GetOrPost or ParameterType.QueryString;
289+
290+
IEnumerable<Parameter> CreateHeaderParameters() => [new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader())];
247291

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

@@ -254,7 +298,7 @@ string GetAuthorizationHeader() {
254298
.Select(x => x.GetQueryParameter(true))
255299
.ToList();
256300

257-
if (!Realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm)}\"");
301+
if (!realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(realm)}\"");
258302

259303
return $"OAuth {string.Join(",", oathParameters)}";
260304
}

src/RestSharp/Authenticators/OAuth/OAuthTools.cs

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public static string GetNonce() {
132132
/// </summary>
133133
/// <param name="parameters"></param>
134134
/// <returns></returns>
135-
static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters));
135+
internal static string NormalizeRequestParameters(WebPairCollection parameters) => string.Join("&", SortParametersExcludingSignature(parameters));
136136

137137
/// <summary>
138138
/// Sorts a <see cref="WebPairCollection" /> by name, and then value if equal.
@@ -193,24 +193,7 @@ public static string GetSignature(
193193
string signatureBase,
194194
string? consumerSecret
195195
)
196-
=> GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret, null);
197-
198-
/// <summary>
199-
/// Creates a signature value given a signature base and the consumer secret.
200-
/// This method is used when the token secret is currently unknown.
201-
/// </summary>
202-
/// <param name="signatureMethod">The hashing method</param>
203-
/// <param name="signatureTreatment">The treatment to use on a signature value</param>
204-
/// <param name="signatureBase">The signature base</param>
205-
/// <param name="consumerSecret">The consumer key</param>
206-
/// <returns></returns>
207-
public static string GetSignature(
208-
OAuthSignatureMethod signatureMethod,
209-
OAuthSignatureTreatment signatureTreatment,
210-
string signatureBase,
211-
string? consumerSecret
212-
)
213-
=> GetSignature(signatureMethod, signatureTreatment, signatureBase, consumerSecret, null);
196+
=> GetSignature(signatureMethod, OAuthSignatureTreatment.Escaped, signatureBase, consumerSecret);
214197

215198
/// <summary>
216199
/// 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(
226209
OAuthSignatureTreatment signatureTreatment,
227210
string signatureBase,
228211
string? consumerSecret,
229-
string? tokenSecret
212+
string? tokenSecret = null
230213
) {
231214
if (tokenSecret.IsEmpty()) tokenSecret = string.Empty;
232215
if (consumerSecret.IsEmpty()) consumerSecret = string.Empty;
@@ -250,7 +233,8 @@ public static string GetSignature(
250233
return result;
251234

252235
string GetRsaSignature() {
253-
using var provider = new RSACryptoServiceProvider { PersistKeyInCsp = false };
236+
using var provider = new RSACryptoServiceProvider();
237+
provider.PersistKeyInCsp = false;
254238

255239
provider.FromXmlString(unencodedConsumerSecret);
256240

0 commit comments

Comments
 (0)