Skip to content

Add OAuth2 token lifecycle authenticators (#2361)#2362

Merged
alexeyzimarev merged 12 commits intodevfrom
feature/oauth2-token-lifecycle
Mar 1, 2026
Merged

Add OAuth2 token lifecycle authenticators (#2361)#2362
alexeyzimarev merged 12 commits intodevfrom
feature/oauth2-token-lifecycle

Conversation

@alexeyzimarev
Copy link
Member

Summary

  • Add OAuth2ClientCredentialsAuthenticator for machine-to-machine flows with automatic token acquisition and refresh
  • Add OAuth2RefreshTokenAuthenticator for user token flows with refresh token rotation support
  • Add OAuth2TokenAuthenticator for custom/non-standard flows via a delegate-based token provider
  • Add shared data models: OAuth2TokenResponse (RFC 6749), OAuth2TokenRequest, OAuth2Token

Each authenticator manages the full token lifecycle internally using its own HttpClient for token endpoint calls, eliminating the circular dependency described in #2101.

Closes #2361

Design

  • Authenticators own a separate HttpClient for token endpoint calls (user can provide their own via OAuth2TokenRequest.HttpClient)
  • Thread-safe via SemaphoreSlim with double-check pattern
  • Action<OAuth2TokenResponse> callback for persisting refreshed tokens
  • Token response follows RFC 6749 Section 5.1 format (works with most OAuth2 servers out of the box)
  • All new files, no changes to existing APIs

Test plan

  • 7 tests for client credentials (obtain, cache, refresh, callback, errors, scope)
  • 6 tests for refresh token (initial use, refresh, rotation, callback, errors)
  • 4 tests for generic authenticator (delegate invocation, caching, expiry, custom token type)
  • 264/264 unit tests pass on net8.0 and net9.0
  • 142/143 integrated tests pass (1 pre-existing NTLM test isolation failure)
  • Build succeeds across all 6 TFMs (netstandard2.0, net471, net48, net8.0, net9.0, net10.0)

🤖 Generated with Claude Code

alexeyzimarev and others added 9 commits March 1, 2026 16:14
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Contributor

Review Summary by Qodo

Add OAuth2 token lifecycle authenticators with automatic acquisition and refresh

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add three new OAuth2 authenticators for automatic token lifecycle management
  - OAuth2ClientCredentialsAuthenticator for machine-to-machine flows
  - OAuth2RefreshTokenAuthenticator for user token flows with refresh rotation
  - OAuth2TokenAuthenticator for custom/delegate-based token providers
• Add shared OAuth2 data models following RFC 6749 Section 5.1
  - OAuth2TokenResponse for token endpoint responses
  - OAuth2TokenRequest for token endpoint configuration
  - OAuth2Token for delegate-based authenticator return type
• Implement thread-safe token caching and automatic refresh with SemaphoreSlim
• Support custom HttpClient injection and token refresh callbacks for persistence
Diagram
flowchart LR
  A["RestRequest"] -->|Authenticate| B["OAuth2 Authenticator"]
  B -->|Token expired?| C["Acquire SemaphoreSlim"]
  C -->|POST to token endpoint| D["Own HttpClient"]
  D -->|Parse response| E["OAuth2TokenResponse"]
  E -->|Cache token| F["Add Bearer header"]
  F -->|Return| A
  E -->|Fire callback| G["OnTokenRefreshed"]
Loading

Grey Divider

File Changes

1. src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs ✨ Enhancement +38/-0

RFC 6749 token response model with JSON deserialization

src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs


2. src/RestSharp/Authenticators/OAuth2/OAuth2Token.cs ✨ Enhancement +22/-0

Simple record for delegate-based authenticator return type

src/RestSharp/Authenticators/OAuth2/OAuth2Token.cs


3. src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs ✨ Enhancement +75/-0

Shared configuration for token endpoint requests

src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs


View more (8)
4. src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs ✨ Enhancement +107/-0

Machine-to-machine OAuth2 flow with automatic token refresh

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs


5. src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs ✨ Enhancement +121/-0

User token flow with refresh token rotation support

src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs


6. src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs ✨ Enhancement +66/-0

Generic delegate-based authenticator for custom flows

src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs


7. test/RestSharp.Tests/Auth/OAuth2ClientCredentialsAuthenticatorTests.cs 🧪 Tests +175/-0

Tests for client credentials flow and token caching

test/RestSharp.Tests/Auth/OAuth2ClientCredentialsAuthenticatorTests.cs


8. test/RestSharp.Tests/Auth/OAuth2RefreshTokenAuthenticatorTests.cs 🧪 Tests +184/-0

Tests for refresh token flow and token rotation

test/RestSharp.Tests/Auth/OAuth2RefreshTokenAuthenticatorTests.cs


9. test/RestSharp.Tests/Auth/OAuth2TokenAuthenticatorTests.cs 🧪 Tests +97/-0

Tests for delegate-based authenticator and custom token types

test/RestSharp.Tests/Auth/OAuth2TokenAuthenticatorTests.cs


10. docs/plans/2026-03-01-oauth2-token-lifecycle-design.md 📝 Documentation +100/-0

Design documentation for OAuth2 token lifecycle architecture

docs/plans/2026-03-01-oauth2-token-lifecycle-design.md


11. docs/plans/2026-03-01-oauth2-token-lifecycle-plan.md 📝 Documentation +994/-0

Detailed implementation plan with task breakdown

docs/plans/2026-03-01-oauth2-token-lifecycle-plan.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Mar 1, 2026

Code Review by Qodo

🐞 Bugs (11) 📘 Rule violations (0) 📎 Requirement gaps (3)

Grey Divider


Action required

1. IAuthenticator signature changed 📎 Requirement gap ✓ Correctness
Description
The PR changes the public IAuthenticator.Authenticate method signature by adding a
CancellationToken parameter, which is a breaking API change for any third-party IAuthenticator
implementations. This violates the requirement to keep existing authenticator APIs/behavior
unchanged when adding the new OAuth2 lifecycle authenticators.
Code

src/RestSharp/Authenticators/IAuthenticator.cs[R17-18]

public interface IAuthenticator {
-    ValueTask Authenticate(IRestClient client, RestRequest request);
+    ValueTask Authenticate(IRestClient client, RestRequest request, CancellationToken cancellationToken = default);
Evidence
Compliance requires the new OAuth2 lifecycle authenticators to be additive without breaking existing
authenticator APIs; however, the PR modifies the existing IAuthenticator contract (and call site)
by adding a new parameter, which breaks existing implementers.

Do not introduce breaking changes to existing OAuth2 authenticator APIs/behavior
CLAUDE.md
src/RestSharp/Authenticators/IAuthenticator.cs[17-19]
src/RestSharp/RestClient.Async.cs[108-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`IAuthenticator.Authenticate` was changed to include an optional `CancellationToken`, which is a breaking public API change for any external implementers of `IAuthenticator`.
## Issue Context
The compliance checklist requires adding new OAuth2 lifecycle authenticators without breaking existing authenticator APIs/behavior. Changing the `IAuthenticator` method signature violates that constraint.
## Fix Focus Areas
- src/RestSharp/Authenticators/IAuthenticator.cs[17-19]
- src/RestSharp/Authenticators/AuthenticatorBase.cs[17-23]
- src/RestSharp/RestClient.Async.cs[108-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. _getToken ignores cancellation📎 Requirement gap ⛯ Reliability
Description
OAuth2TokenAuthenticator invokes the token provider with CancellationToken.None, so token
acquisition cannot be cancelled. This violates the requirement that the delegate-based lifecycle
authenticator supports cancellation via CancellationToken.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs[R54-56]

+            var result = await _getToken(CancellationToken.None).ConfigureAwait(false);
+            _accessToken = result.AccessToken;
+            _tokenExpiry = result.ExpiresAt;
Evidence
PR Compliance ID 3 requires the delegate-based authenticator to be cancellable via
CancellationToken. The new implementation always calls the delegate with CancellationToken.None,
preventing cancellation from being propagated to the token acquisition code.

Implement delegate-based OAuth2 token lifecycle authenticator with caching and expiry handling
src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs[54-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`OAuth2TokenAuthenticator` calls the user-supplied `Func&amp;amp;amp;lt;CancellationToken, Task&amp;amp;amp;lt;OAuth2Token&amp;amp;amp;gt;&amp;amp;amp;gt;` with `CancellationToken.None`, so token acquisition cannot be cancelled.
## Issue Context
Compliance requires that the delegate-based lifecycle authenticator be cancellable via `CancellationToken`. RestSharp&amp;amp;amp;#x27;s `IAuthenticator.Authenticate(IRestClient, RestRequest)` does not currently accept a cancellation token, so you will need to route the execution cancellation token to the authenticator via an alternative mechanism (e.g., store it on the `RestRequest` before calling `Authenticate`, or introduce a compatible authenticator hook that includes a token).
## Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs[39-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. _getToken ignores cancellation📎 Requirement gap ⛯ Reliability
Description
OAuth2TokenAuthenticator invokes the token provider with CancellationToken.None, so token
acquisition cannot be cancelled. This violates the requirement that the delegate-based lifecycle
authenticator supports cancellation via CancellationToken.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs[R54-56]

+            var result = await _getToken(CancellationToken.None).ConfigureAwait(false);
+            _accessToken = result.AccessToken;
+            _tokenExpiry = result.ExpiresAt;
Evidence
PR Compliance ID 3 requires the delegate-based authenticator to be cancellable via
CancellationToken. The new implementation always calls the delegate with CancellationToken.None,
preventing cancellation from being propagated to the token acquisition code.

Implement delegate-based OAuth2 token lifecycle authenticator with caching and expiry handling
src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs[54-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`OAuth2TokenAuthenticator` calls the user-supplied `Func&amp;amp;amp;amp;lt;CancellationToken, Task&amp;amp;amp;amp;lt;OAuth2Token&amp;amp;amp;amp;gt;&amp;amp;amp;amp;gt;` with `CancellationToken.None`, so token acquisition cannot be cancelled.
## Issue Context
Compliance requires that the delegate-based lifecycle authenticator be cancellable via `CancellationToken`. RestSharp&amp;amp;amp;amp;#x27;s `IAuthenticator.Authenticate(IRestClient, RestRequest)` does not currently accept a cancellation token, so you will need to route the execution cancellation token to the authenticator via an alternative mechanism (e.g., store it on the `RestRequest` before calling `Authenticate`, or introduce a compatible authenticator hook that includes a token).
## Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenAuthenticator.cs[39-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (4)
4. Callback deadlock under lock🐞 Bug ⛯ Reliability
Description
OnTokenRefreshed is invoked while holding the SemaphoreSlim refresh lock. If the callback triggers
another request using the same authenticator (directly or indirectly), it can deadlock; even without
deadlock it blocks all concurrent requests waiting for the lock.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[R91-100]

+            _accessToken = tokenResponse.AccessToken;
+            _tokenExpiry = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn) - _request.ExpiryBuffer;
+
+            _request.OnTokenRefreshed?.Invoke(tokenResponse);
+
+            return _accessToken;
+        }
+        finally {
+            _lock.Release();
+        }
Evidence
Both authenticators invoke the user callback before releasing the refresh semaphore. RestClient
calls Authenticate during request execution, so a callback that performs a RestSharp request using
the same authenticator can re-enter Authenticate and block forever on the same semaphore.

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[57-100]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[70-114]
src/RestSharp/RestClient.Async.cs[108-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`OnTokenRefreshed` is executed while holding the internal refresh `SemaphoreSlim`. User code in that callback can block, perform I/O, or re-enter RestSharp and cause deadlocks. It also unnecessarily blocks all other waiting requests.
### Issue Context
Both `OAuth2ClientCredentialsAuthenticator` and `OAuth2RefreshTokenAuthenticator` call `_request.OnTokenRefreshed?.Invoke(tokenResponse)` before releasing `_lock`.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[57-100]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[70-114]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Callback deadlock under lock🐞 Bug ⛯ Reliability
Description
OnTokenRefreshed is invoked while holding the SemaphoreSlim refresh lock. If the callback triggers
another request using the same authenticator (directly or indirectly), it can deadlock; even without
deadlock it blocks all concurrent requests waiting for the lock.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[R91-100]

+            _accessToken = tokenResponse.AccessToken;
+            _tokenExpiry = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn) - _request.ExpiryBuffer;
+
+            _request.OnTokenRefreshed?.Invoke(tokenResponse);
+
+            return _accessToken;
+        }
+        finally {
+            _lock.Release();
+        }
Evidence
Both authenticators invoke the user callback before releasing the refresh semaphore. RestClient
calls Authenticate during request execution, so a callback that performs a RestSharp request using
the same authenticator can re-enter Authenticate and block forever on the same semaphore.

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[57-100]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[70-114]
src/RestSharp/RestClient.Async.cs[108-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`OnTokenRefreshed` is executed while holding the internal refresh `SemaphoreSlim`. User code in that callback can block, perform I/O, or re-enter RestSharp and cause deadlocks. It also unnecessarily blocks all other waiting requests.
### Issue Context
Both `OAuth2ClientCredentialsAuthenticator` and `OAuth2RefreshTokenAuthenticator` call `_request.OnTokenRefreshed?.Invoke(tokenResponse)` before releasing `_lock`.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[57-100]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[70-114]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Missing expires_in causes refresh storm🐞 Bug ⛯ Reliability
Description
OAuth2TokenResponse.ExpiresIn is a non-nullable int; when expires_in is omitted (allowed by RFC
6749), it deserializes to 0 and the code treats the token as immediately expired, causing a token
endpoint call on every request.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[R27-32]

+    [JsonPropertyName("token_type")]
+    public string TokenType { get; init; } = "";
+
+    [JsonPropertyName("expires_in")]
+    public int ExpiresIn { get; init; }
+
Evidence
The response model makes expires_in default to 0 when absent, and both authenticators compute expiry
solely from ExpiresIn (and subtract ExpiryBuffer). This makes missing expires_in behave like
'already expired', leading to repeated refresh attempts.

src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[23-32]
src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[86-96]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[96-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When the token endpoint omits `expires_in`, deserialization yields `0` and the authenticators treat the token as expired immediately. This can cause a refresh/token acquisition call for every request.
### Issue Context
`OAuth2TokenResponse.ExpiresIn` is a non-nullable `int` and expiry computation in authenticators uses it unconditionally.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[23-32]
- src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[86-96]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[96-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Missing expires_in causes refresh storm🐞 Bug ⛯ Reliability
Description
OAuth2TokenResponse.ExpiresIn is a non-nullable int; when expires_in is omitted (allowed by RFC
6749), it deserializes to 0 and the code treats the token as immediately expired, causing a token
endpoint call on every request.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[R27-32]

+    [JsonPropertyName("token_type")]
+    public string TokenType { get; init; } = "";
+
+    [JsonPropertyName("expires_in")]
+    public int ExpiresIn { get; init; }
+
Evidence
The response model makes expires_in default to 0 when absent, and both authenticators compute expiry
solely from ExpiresIn (and subtract ExpiryBuffer). This makes missing expires_in behave like
'already expired', leading to repeated refresh attempts.

src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[23-32]
src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[86-96]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[96-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When the token endpoint omits `expires_in`, deserialization yields `0` and the authenticators treat the token as expired immediately. This can cause a refresh/token acquisition call for every request.
### Issue Context
`OAuth2TokenResponse.ExpiresIn` is a non-nullable `int` and expiry computation in authenticators uses it unconditionally.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[23-32]
- src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[86-96]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[96-103]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

8. Refresh flow ignores Scope🐞 Bug ✓ Correctness
Description
OAuth2RefreshTokenAuthenticator never sends OAuth2TokenRequest.Scope, unlike the client-credentials
authenticator. Some servers require/validate scope on refresh, so refresh may fail or return
unexpected scope.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[R76-86]

+            var parameters = new Dictionary<string, string> {
+                ["grant_type"]    = "refresh_token",
+                ["client_id"]     = _request.ClientId,
+                ["client_secret"] = _request.ClientSecret,
+                ["refresh_token"] = _refreshToken
+            };
+
+            if (_request.ExtraParameters != null) {
+                foreach (var kvp in _request.ExtraParameters)
+                    parameters[kvp.Key] = kvp.Value;
+            }
Evidence
OAuth2TokenRequest has a Scope property and the client-credentials authenticator includes it in the
form payload, but refresh-token authenticator does not.

src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs[50-54]
src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[64-72]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[76-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`OAuth2RefreshTokenAuthenticator` does not include `scope` in the refresh request even when configured.
### Issue Context
`OAuth2TokenRequest` exposes `Scope`, and client-credentials flow includes it; refresh-token flow does not.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[76-86]
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs[50-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. ExpiryBuffer not on initial token 🐞 Bug ⛯ Reliability
Description
ExpiryBuffer is applied when computing expiry from expires_in, but not applied to the initial
expiresAt passed into OAuth2RefreshTokenAuthenticator. This inconsistency can cause near-expiry
requests to be sent with a token that should have been treated as stale per the buffer policy.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[R47-50]

+    protected void SetInitialToken(string accessToken, DateTimeOffset expiresAt) {
+        _accessToken = accessToken;
+        _tokenExpiry = expiresAt;
+    }
Evidence
Initial expiry is set verbatim, while refreshed token expiry subtracts the configured ExpiryBuffer.
This makes the initial token behave differently than subsequently refreshed tokens under the same
configuration.

src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[47-50]
src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[98-101]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ExpiryBuffer` is not applied to the initial token expiry in refresh-token flows, but it is applied to refreshed tokens.
## Issue Context
Users expect `ExpiryBuffer` to uniformly define when a token becomes stale.
## Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[47-50]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[31-39]
## Suggested approach
- In `SetInitialToken`, set `_tokenExpiry = expiresAt - TokenRequest.ExpiryBuffer` (possibly clamp so it never exceeds `expiresAt` and doesn’t underflow).
- Add/adjust unit tests to assert initial token is treated as stale `ExpiryBuffer` before its actual expiry.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Token type hardcoded Bearer🐞 Bug ✓ Correctness
Description
Client-credentials and refresh-token authenticators always emit an Authorization header with
"Bearer" and ignore token_type from the token endpoint response; this reduces compatibility with
non-Bearer token types.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[R48-51]

+    public async ValueTask Authenticate(IRestClient client, RestRequest request) {
+        var token = await GetOrRefreshTokenAsync().ConfigureAwait(false);
+        request.AddOrUpdateParameter(new HeaderParameter(KnownHeaders.Authorization, $"Bearer {token}"));
+    }
Evidence
OAuth2TokenResponse models token_type, and existing OAuth2 header authenticator supports
configurable token type, but the lifecycle authenticators hardcode "Bearer".

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[48-51]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[61-64]
src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[24-32]
src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs[24-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Lifecycle authenticators hardcode `Bearer` and ignore the `token_type` returned by the token endpoint.
### Issue Context
`OAuth2TokenResponse` includes `TokenType`, and existing OAuth2 header authenticator supports a configurable token type.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[48-51]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[61-64]
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[24-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
11. No expires_in cached forever 🐞 Bug ⛯ Reliability
Description
When expires_in is omitted, the token is treated as non-expiring and cached until
DateTimeOffset.MaxValue. If a server omits expires_in but the token still expires, the
authenticator will never refresh and will keep sending an invalid token until callers manually
intervene.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[R99-101]

+            _tokenExpiry = tokenResponse.ExpiresIn.HasValue
+                ? DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn.Value) - TokenRequest.ExpiryBuffer
+                : DateTimeOffset.MaxValue;
Evidence
The implementation explicitly maps missing expires_in to a never-expiring cache entry, and the
response model makes ExpiresIn nullable, enabling this path.

src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[98-101]
src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[29-37]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Missing `expires_in` currently results in caching the access token indefinitely (`DateTimeOffset.MaxValue`). This can break providers that omit `expires_in` while still expiring tokens.
## Issue Context
RFC allows omitting `expires_in` (it’s optional), but real providers may still expire tokens.
## Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2EndpointAuthenticatorBase.cs[98-101]
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs[66-74]
## Suggested approach
- Option 1 (document): Clearly document that missing `expires_in` is treated as non-expiring.
- Option 2 (configurable): Add a `TimeSpan? FallbackExpiresIn` (or similar) on `OAuth2TokenRequest`.
- If `ExpiresIn` is null and `FallbackExpiresIn` is set, use it.
- If both are null, decide whether to cache indefinitely or force refresh each time.
- Add unit tests for the chosen behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Refresh flow ignores Scope🐞 Bug ✓ Correctness
Description
OAuth2RefreshTokenAuthenticator never sends OAuth2TokenRequest.Scope, unlike the client-credentials
authenticator. Some servers require/validate scope on refresh, so refresh may fail or return
unexpected scope.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[R76-86]

+            var parameters = new Dictionary<string, string> {
+                ["grant_type"]    = "refresh_token",
+                ["client_id"]     = _request.ClientId,
+                ["client_secret"] = _request.ClientSecret,
+                ["refresh_token"] = _refreshToken
+            };
+
+            if (_request.ExtraParameters != null) {
+                foreach (var kvp in _request.ExtraParameters)
+                    parameters[kvp.Key] = kvp.Value;
+            }
Evidence
OAuth2TokenRequest has a Scope property and the client-credentials authenticator includes it in the
form payload, but refresh-token authenticator does not.

src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs[50-54]
src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[64-72]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[76-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`OAuth2RefreshTokenAuthenticator` does not include `scope` in the refresh request even when configured.
### Issue Context
`OAuth2TokenRequest` exposes `Scope`, and client-credentials flow includes it; refresh-token flow does not.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[76-86]
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenRequest.cs[50-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Token type hardcoded Bearer🐞 Bug ✓ Correctness
Description
Client-credentials and refresh-token authenticators always emit an Authorization header with
"Bearer" and ignore token_type from the token endpoint response; this reduces compatibility with
non-Bearer token types.
Code

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[R48-51]

+    public async ValueTask Authenticate(IRestClient client, RestRequest request) {
+        var token = await GetOrRefreshTokenAsync().ConfigureAwait(false);
+        request.AddOrUpdateParameter(new HeaderParameter(KnownHeaders.Authorization, $"Bearer {token}"));
+    }
Evidence
OAuth2TokenResponse models token_type, and existing OAuth2 header authenticator supports
configurable token type, but the lifecycle authenticators hardcode "Bearer".

src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[48-51]
src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[61-64]
src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[24-32]
src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs[24-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Lifecycle authenticators hardcode `Bearer` and ignore the `token_type` returned by the token endpoint.
### Issue Context
`OAuth2TokenResponse` includes `TokenType`, and existing OAuth2 header authenticator supports a configurable token type.
### Fix Focus Areas
- src/RestSharp/Authenticators/OAuth2/OAuth2ClientCredentialsAuthenticator.cs[48-51]
- src/RestSharp/Authenticators/OAuth2/OAuth2RefreshTokenAuthenticator.cs[61-64]
- src/RestSharp/Authenticators/OAuth2/OAuth2TokenResponse.cs[24-32]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

14. AuthenticatorBase can't cancel work🐞 Bug ⛯ Reliability
Description
AuthenticatorBase accepts a CancellationToken but does not forward it to an overridable method, so
derived authenticators cannot implement cancellable auth work via the base class’ template method.
Code

src/RestSharp/Authenticators/AuthenticatorBase.cs[R20-23]

   protected abstract ValueTask<Parameter> GetAuthenticationParameter(string accessToken);

-    public async ValueTask Authenticate(IRestClient client, RestRequest request)
+    public async ValueTask Authenticate(IRestClient client, RestRequest request, CancellationToken cancellationToken = default)
       => request.AddOrUpdateParameter(await GetAuthenticationParameter(Token).ConfigureAwait(false));
Evidence
The new CancellationToken parameter is unused in the base implementation and
GetAuthenticationParameter cannot observe cancellation. This is mostly relevant if consumers build
custom authenticators on top of AuthenticatorBase that do I/O and want cancellation.

src/RestSharp/Authenticators/AuthenticatorBase.cs[20-23]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`AuthenticatorBase` cannot propagate cancellation because `GetAuthenticationParameter` doesn’t accept a `CancellationToken`.
## Issue Context
While built-in stampers are synchronous, consumers may derive from `AuthenticatorBase` for authenticators that do async work.
## Fix Focus Areas
- src/RestSharp/Authenticators/AuthenticatorBase.cs[17-23]
## Suggested approach
- Add a new virtual method:
- `protected virtual ValueTask&amp;lt;Parameter&amp;gt; GetAuthenticationParameter(string accessToken, CancellationToken cancellationToken)`
- default implementation calls the existing abstract method.
- Update `Authenticate` to call the new overload, passing `cancellationToken`.
This is additive for derived classes (no breaking override required) while enabling cancellation-aware implementations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Mar 1, 2026

Deploying restsharp with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2674757
Status: ✅  Deploy successful!
Preview URL: https://90627df5.restsharp.pages.dev
Branch Preview URL: https://feature-oauth2-token-lifecyc.restsharp.pages.dev

View logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
OAuth2ClientCredentialsAuthenticator and OAuth2RefreshTokenAuthenticator
shared ~60 lines of identical code for HttpClient management, locking,
token parsing, error handling, and disposal. Extract into
OAuth2EndpointAuthenticatorBase. Subclasses now only provide
grant-specific parameters and post-response hooks.

Fixes SonarCloud duplication gate (4.5% > 3% threshold).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link

github-actions bot commented Mar 1, 2026

Test Results

   42 files     42 suites   19m 18s ⏱️
  560 tests   560 ✅ 0 💤 0 ❌
3 908 runs  3 908 ✅ 0 💤 0 ❌

Results for commit 2674757.

♻️ This comment has been updated with latest results.

…e on refresh

- Add optional CancellationToken to IAuthenticator.Authenticate and propagate
  it through all authenticators to SemaphoreSlim.WaitAsync, HttpClient.PostAsync,
  and the user delegate in OAuth2TokenAuthenticator
- Make OAuth2TokenResponse.ExpiresIn nullable (int?) so missing expires_in from
  the server is treated as non-expiring instead of causing a refresh storm
- Send scope parameter in OAuth2RefreshTokenAuthenticator when configured

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 1, 2026

@alexeyzimarev
Copy link
Member Author

/review

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Mar 1, 2026

Persistent review updated to latest commit 2674757

Comment on lines 17 to +18
public interface IAuthenticator {
ValueTask Authenticate(IRestClient client, RestRequest request);
ValueTask Authenticate(IRestClient client, RestRequest request, CancellationToken cancellationToken = default);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. iauthenticator signature changed 📎 Requirement gap ✓ Correctness

The PR changes the public IAuthenticator.Authenticate method signature by adding a
CancellationToken parameter, which is a breaking API change for any third-party IAuthenticator
implementations. This violates the requirement to keep existing authenticator APIs/behavior
unchanged when adding the new OAuth2 lifecycle authenticators.
Agent Prompt
## Issue description
`IAuthenticator.Authenticate` was changed to include an optional `CancellationToken`, which is a breaking public API change for any external implementers of `IAuthenticator`.

## Issue Context
The compliance checklist requires adding new OAuth2 lifecycle authenticators without breaking existing authenticator APIs/behavior. Changing the `IAuthenticator` method signature violates that constraint.

## Fix Focus Areas
- src/RestSharp/Authenticators/IAuthenticator.cs[17-19]
- src/RestSharp/Authenticators/AuthenticatorBase.cs[17-23]
- src/RestSharp/RestClient.Async.cs[108-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@alexeyzimarev alexeyzimarev merged commit 47ca314 into dev Mar 1, 2026
12 checks passed
@alexeyzimarev alexeyzimarev deleted the feature/oauth2-token-lifecycle branch March 1, 2026 16:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add OAuth2 token lifecycle authenticators (client credentials, refresh token, custom provider)

1 participant