Skip to content
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
94 changes: 90 additions & 4 deletions docs/docs/advanced/authenticators.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,95 @@ var authenticator = OAuth1Authenticator.ForAccessToken(

## OAuth2

RestSharp has two very simple authenticators to send the access token as part of the request.
RestSharp provides OAuth2 authenticators at two levels: **token lifecycle authenticators** that handle the full flow (obtaining, caching, and refreshing tokens automatically), and **simple authenticators** that just stamp a pre-obtained token onto requests.

### Token lifecycle authenticators

These authenticators manage tokens end-to-end. They use their own internal `HttpClient` for token endpoint calls, so there's no circular dependency with the `RestClient` they're attached to. All are thread-safe for concurrent use.

#### Client credentials

Use `OAuth2ClientCredentialsAuthenticator` for machine-to-machine flows. It POSTs `grant_type=client_credentials` to your token endpoint, caches the token, and refreshes it automatically before it expires.

```csharp
var request = new OAuth2TokenRequest(
"https://auth.example.com/oauth2/token",
"my-client-id",
"my-client-secret"
) {
Scope = "api.read api.write"
};

var options = new RestClientOptions("https://api.example.com") {
Authenticator = new OAuth2ClientCredentialsAuthenticator(request)
};
using var client = new RestClient(options);
```

The authenticator will obtain a token on the first request and reuse it until it expires. The `ExpiryBuffer` property (default 30 seconds) controls how far in advance of actual expiry the token is considered stale.

#### Refresh token

Use `OAuth2RefreshTokenAuthenticator` when you already have an access token and refresh token (e.g., from an authorization code flow). It uses the initial access token until it expires, then automatically refreshes using the `refresh_token` grant type.

```csharp
var request = new OAuth2TokenRequest(
"https://auth.example.com/oauth2/token",
"my-client-id",
"my-client-secret"
) {
OnTokenRefreshed = response => {
// Persist the new tokens to your storage
SaveTokens(response.AccessToken, response.RefreshToken);
}
};

var options = new RestClientOptions("https://api.example.com") {
Authenticator = new OAuth2RefreshTokenAuthenticator(
request,
accessToken: "current-access-token",
refreshToken: "current-refresh-token",
expiresAt: DateTimeOffset.UtcNow.AddMinutes(30)
)
};
using var client = new RestClient(options);
```

If the server rotates refresh tokens, the authenticator will automatically use the new refresh token for subsequent refreshes. The `OnTokenRefreshed` callback fires every time a new token is obtained, so you can persist the updated tokens.

#### Custom token provider

Use `OAuth2TokenAuthenticator` when you have a non-standard token flow or want full control over how tokens are obtained. Provide an async delegate that returns an `OAuth2Token`:

```csharp
var options = new RestClientOptions("https://api.example.com") {
Authenticator = new OAuth2TokenAuthenticator(async cancellationToken => {
var token = await myCustomTokenService.GetTokenAsync(cancellationToken);
return new OAuth2Token(token.Value, token.ExpiresAt);
})
};
using var client = new RestClient(options);
```

The authenticator caches the result and re-invokes your delegate when the token expires.

#### Bringing your own HttpClient

By default, the token lifecycle authenticators create their own `HttpClient` for token endpoint calls (and dispose it when the authenticator is disposed). If you need to customize it (e.g., for proxy settings or mTLS), pass your own:

```csharp
var request = new OAuth2TokenRequest(
"https://auth.example.com/oauth2/token",
"my-client-id",
"my-client-secret"
) {
HttpClient = myCustomHttpClient // not disposed by the authenticator
};
```

### Simple authenticators

If you manage tokens yourself and just need to stamp them onto requests, use these simpler authenticators.

`OAuth2UriQueryParameterAuthenticator` accepts the access token as the only constructor argument, and it will send the provided token as a query parameter `oauth_token`.

Expand All @@ -148,8 +236,6 @@ var client = new RestClient(options);

The code above will tell RestSharp to send the bearer token with each request as a header. Essentially, the code above does the same as the sample for `JwtAuthenticator` below.

As those authenticators don't do much to get the token itself, you might be interested in looking at our [sample OAuth2 authenticator](../usage/example.md#authenticator), which requests the token on its own.

## JWT

The JWT authentication can be supported by using `JwtAuthenticator`. It is a very simple class that can be constructed like this:
Expand Down Expand Up @@ -182,4 +268,4 @@ var client = new RestClient(options);
The `Authenticate` method is the very first thing called upon calling `RestClient.Execute` or `RestClient.Execute<T>`.
It gets the `RestRequest` currently being executed giving you access to every part of the request data (headers, parameters, etc.)

You can find an example of a custom authenticator that fetches and uses an OAuth2 bearer token [here](../usage/example.md#authenticator).
You can find an example of using the built-in OAuth2 authenticator in a typed API client [here](../usage/example.md#authenticator).
81 changes: 21 additions & 60 deletions docs/docs/usage/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ public class TwitterClient : ITwitterClient, IDisposable {
readonly RestClient _client;

public TwitterClient(string apiKey, string apiKeySecret) {
var options = new RestClientOptions("https://api.twitter.com/2");
var tokenRequest = new OAuth2TokenRequest(
"https://api.twitter.com/oauth2/token",
apiKey,
apiKeySecret
);
var options = new RestClientOptions("https://api.twitter.com/2") {
Authenticator = new OAuth2ClientCredentialsAuthenticator(tokenRequest)
};
_client = new RestClient(options);
}

Expand Down Expand Up @@ -79,73 +86,27 @@ public TwitterClient(IOptions<TwitterClientOptions> options) {

Then, you can register and configure the client using ASP.NET Core dependency injection container.

Right now, the client won't really work as Twitter API requires authentication. It's covered in the next section.
Notice the client constructor already configures the `OAuth2ClientCredentialsAuthenticator`. The authenticator setup is described in the next section.

## Authenticator

Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the OAuth2 conventions, the code can be used to create an authenticator for some other vendors.

First, we need a model for deserializing the token endpoint response. OAuth2 uses snake case for property naming, so we need to decorate model properties with `JsonPropertyName` attribute:

```csharp
record TokenResponse {
[JsonPropertyName("token_type")]
public string TokenType { get; init; }
[JsonPropertyName("access_token")]
public string AccessToken { get; init; }
}
```

Next, we create the authenticator itself. It needs the API key and API key secret to call the token endpoint using basic HTTP authentication. In addition, we can extend the list of parameters with the base URL to convert it to a more generic OAuth2 authenticator.

The easiest way to create an authenticator is to inherit from the `AuthenticatorBase` base class:

```csharp
public class TwitterAuthenticator : AuthenticatorBase {
readonly string _baseUrl;
readonly string _clientId;
readonly string _clientSecret;

public TwitterAuthenticator(string baseUrl, string clientId, string clientSecret) : base("") {
_baseUrl = baseUrl;
_clientId = clientId;
_clientSecret = clientSecret;
}

protected override async ValueTask<Parameter> GetAuthenticationParameter(string accessToken) {
Token = string.IsNullOrEmpty(Token) ? await GetToken() : Token;
return new HeaderParameter(KnownHeaders.Authorization, Token);
}
}
```

During the first call made by the client using the authenticator, it will find out that the `Token` property is empty. It will then call the `GetToken` function to get the token once and reuse the token going forward.

Now, we need to implement the `GetToken` function in the class:
Before we can call the API itself, we need to get a bearer token. Twitter exposes an endpoint `https://api.twitter.com/oauth2/token`. As it follows the standard OAuth2 client credentials convention, we can use the built-in `OAuth2ClientCredentialsAuthenticator`:

```csharp
async Task<string> GetToken() {
var options = new RestClientOptions(_baseUrl){
Authenticator = new HttpBasicAuthenticator(_clientId, _clientSecret),
};
using var client = new RestClient(options);

var request = new RestRequest("oauth2/token")
.AddParameter("grant_type", "client_credentials");
var response = await client.PostAsync<TokenResponse>(request);
return $"{response!.TokenType} {response!.AccessToken}";
}
var tokenRequest = new OAuth2TokenRequest(
"https://api.twitter.com/oauth2/token",
apiKey,
apiKeySecret
);

var options = new RestClientOptions("https://api.twitter.com/2") {
Authenticator = new OAuth2ClientCredentialsAuthenticator(tokenRequest)
};
```

As we need to make a call to the token endpoint, we need our own short-lived instance of `RestClient`. Unlike the actual Twitter client, it will use the `HttpBasicAuthenticator` to send the API key and secret as the username and password. The client then gets disposed as we only use it once.

Here we add a POST parameter `grant_type` with `client_credentials` as its value. At the moment, it's the only supported value.

The POST request will use the `application/x-www-form-urlencoded` content type by default.
The authenticator will automatically obtain a token on the first request, cache it, and refresh it when it expires. It uses its own `HttpClient` internally for token endpoint calls, so there's no circular dependency with the `RestClient`.

::: note
Sample code provided on this page is a production code. For example, the authenticator might produce undesired side effect when multiple requests are made at the same time when the token hasn't been obtained yet. It can be solved rather than simply using semaphores or synchronized invocation.
:::
For more details on the available OAuth2 authenticators (including refresh token flows and custom token providers), see [Authenticators](../advanced/authenticators.md#oauth2).

## Final words

Expand Down
100 changes: 100 additions & 0 deletions docs/plans/2026-03-01-oauth2-token-lifecycle-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# OAuth2 Token Lifecycle Authenticators

**Issue:** [#2101](https://github.com/restsharp/RestSharp/issues/2101)
**Date:** 2026-03-01

## Problem

The existing OAuth2 authenticators are static token stampers — they take a pre-obtained token and add it to requests. Users who need automatic token acquisition, caching, and refresh hit a circular dependency: the authenticator needs an HttpClient to call the token endpoint, but it lives inside the RestClient it's attached to.

## Solution

Self-contained OAuth2 authenticators that manage the full token lifecycle using their own internal HttpClient for token endpoint calls.

## Components

### OAuth2TokenResponse

RFC 6749 Section 5.1 token response model. Used for deserializing token endpoint responses.

Fields: `AccessToken`, `TokenType`, `ExpiresIn`, `RefreshToken` (optional), `Scope` (optional). Deserialized with `System.Text.Json` using `JsonPropertyName` attributes for snake_case mapping.

### OAuth2TokenRequest

Shared configuration for token endpoint calls.

- `TokenEndpointUrl` (required) — URL of the OAuth2 token endpoint
- `ClientId` (required) — OAuth2 client ID
- `ClientSecret` (required) — OAuth2 client secret
- `Scope` (optional) — requested scope
- `ExtraParameters` (optional) — additional form parameters
- `HttpClient` (optional) — bring your own HttpClient for token calls
- `ExpiryBuffer` — refresh before actual expiry (default 30s)
- `OnTokenRefreshed` — callback fired when a new token is obtained

### OAuth2Token

Simple record `(string AccessToken, DateTimeOffset ExpiresAt)` for the generic authenticator's delegate return type.

### OAuth2ClientCredentialsAuthenticator

Machine-to-machine flow. POSTs `grant_type=client_credentials` to the token endpoint. Caches the token and refreshes when expired. Thread-safe via SemaphoreSlim with double-check pattern. Implements IDisposable to clean up owned HttpClient.

### OAuth2RefreshTokenAuthenticator

User token flow. Takes initial access + refresh tokens. When the access token expires, POSTs `grant_type=refresh_token`. Updates the cached refresh token if the server rotates it. Fires `OnTokenRefreshed` callback so callers can persist new tokens.

### OAuth2TokenAuthenticator

Generic/delegate-based. Takes `Func<CancellationToken, Task<OAuth2Token>>`. For non-standard flows where users provide their own token acquisition logic. Caches the result and re-invokes the delegate on expiry.

## Data Flow

```
Request → Authenticate()
→ cached token valid? → stamp Authorization header
→ expired? → acquire SemaphoreSlim
→ double-check still expired
→ POST to token endpoint (own HttpClient)
→ parse OAuth2TokenResponse
→ cache token, compute expiry (ExpiresIn - ExpiryBuffer)
→ fire OnTokenRefreshed callback
→ stamp Authorization header
```

## Error Handling

- Non-2xx from token endpoint: throw HttpRequestException with status and body
- Missing access_token in response: throw InvalidOperationException
- No retry logic — callers control retries at RestClient level

## Thread Safety

SemaphoreSlim(1, 1) with double-check pattern. One thread refreshes; concurrent callers wait and reuse the new token.

## IDisposable

Authenticators that create their own HttpClient dispose it. User-provided HttpClient is not disposed. Same pattern as RestClient itself.

## Multi-targeting

System.Text.Json for deserialization — NuGet package on netstandard2.0/net471/net48, built-in on net8.0+. No conditional compilation needed.

## Files

```
src/RestSharp/Authenticators/OAuth2/
OAuth2TokenResponse.cs (new)
OAuth2TokenRequest.cs (new)
OAuth2Token.cs (new)
OAuth2ClientCredentialsAuthenticator.cs (new)
OAuth2RefreshTokenAuthenticator.cs (new)
OAuth2TokenAuthenticator.cs (new)

test/RestSharp.Tests/Auth/
OAuth2ClientCredentialsAuthenticatorTests.cs (new)
OAuth2RefreshTokenAuthenticatorTests.cs (new)
OAuth2TokenAuthenticatorTests.cs (new)
```

No changes to existing files. No API breaks.
Loading
Loading