docs/plans/2026-03-01-oauth2-token-lifecycle-design.md
Issue: #2101 Date: 2026-03-01
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.
Self-contained OAuth2 authenticators that manage the full token lifecycle using their own internal HttpClient for token endpoint calls.
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.
Shared configuration for token endpoint calls.
TokenEndpointUrl (required) — URL of the OAuth2 token endpointClientId (required) — OAuth2 client IDClientSecret (required) — OAuth2 client secretScope (optional) — requested scopeExtraParameters (optional) — additional form parametersHttpClient (optional) — bring your own HttpClient for token callsExpiryBuffer — refresh before actual expiry (default 30s)OnTokenRefreshed — callback fired when a new token is obtainedSimple record (string AccessToken, DateTimeOffset ExpiresAt) for the generic authenticator's delegate return type.
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.
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.
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.
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
SemaphoreSlim(1, 1) with double-check pattern. One thread refreshes; concurrent callers wait and reuse the new token.
Authenticators that create their own HttpClient dispose it. User-provided HttpClient is not disposed. Same pattern as RestClient itself.
System.Text.Json for deserialization — NuGet package on netstandard2.0/net471/net48, built-in on net8.0+. No conditional compilation needed.
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.