/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using QuantConnect.Api; using System.Threading; namespace QuantConnect.Brokerages.Authentication { /// /// Handles OAuth token retrieval and caching by interacting with the Lean platform. /// Implements retry and expiration logic for secure HTTP communication. /// public class LeanOAuthTokenHandler : LeanOAuthTokenHandler { /// /// Initializes a new instance of the class with default token credentials type. /// /// The API client used to communicate with the Lean platform. /// The request model used to generate the access token. /// /// The expected lifetime of a fetched token. A 1-minute safety buffer is applied before expiry. /// Must be provided explicitly — each brokerage has a different token lifetime. /// public LeanOAuthTokenHandler(ApiConnection apiClient, OAuthTokenRequest request, TimeSpan tokenLifetime) : base(apiClient, request, tokenLifetime) { } } /// /// Handles OAuth token retrieval and caching by interacting with the Lean platform. /// Implements retry and expiration logic for secure HTTP communication. /// public class LeanOAuthTokenHandler : LeanTokenHandler where T : LeanTokenCredentials { /// /// The maximum number of retry attempts when fetching an access token. /// protected virtual int MaxRetryCount { get; set; } = 3; /// /// The time interval to wait between retry attempts when fetching an access token. /// protected virtual TimeSpan RetryInterval { get; set; } = TimeSpan.FromSeconds(5); /// /// Lock object used to synchronize token refresh across threads. /// private readonly Lock _lock = new(); /// /// The serialized JSON body representing the token request model. /// private readonly string _jsonBodyRequest; /// /// The total lifetime of a fetched token, used to compute the expiry timestamp. /// A 1-minute safety buffer is subtracted before the token is considered expired. /// private readonly TimeSpan _tokenLifetime; /// /// API client for communicating with the Lean platform. /// private readonly ApiConnection _apiClient; /// /// Stores the current access token and its type used for authenticating requests to the Lean platform. /// Always accessed inside . /// private T _tokenCredentials; /// /// The UTC timestamp after which the cached token should be refreshed. /// Always accessed inside . /// private DateTime _tokenExpiresAt; /// /// Some padding before expiration to request a new token /// public TimeSpan OffsetBeforeExpiration { get; set; } = TimeSpan.FromMinutes(2); /// /// Initializes a new instance of the class. /// /// The API client used to communicate with the Lean platform. /// The request model used to generate the access token. /// /// The expected lifetime of a fetched token. A 1-minute safety buffer is applied before expiry. /// Must be provided explicitly — each brokerage has a different token lifetime. /// public LeanOAuthTokenHandler(ApiConnection apiClient, OAuthTokenRequest request, TimeSpan tokenLifetime) { _apiClient = apiClient; _jsonBodyRequest = request.ToJson(); _tokenLifetime = tokenLifetime; } /// /// Retrieves a valid access token from the Lean platform. /// Caches and reuses tokens until expiration to minimize unnecessary requests. /// Retries up to times on failure. Thread-safe via a lock. /// /// A token used to observe cancellation requests. /// A instance containing the token type and access token string. public override T GetAccessToken(CancellationToken cancellationToken) { lock (_lock) { if (_tokenCredentials != null && DateTime.UtcNow < _tokenExpiresAt) { return _tokenCredentials; } for (var retryCount = 0; retryCount <= MaxRetryCount; retryCount++) { try { using var request = ApiUtils.CreateJsonPostRequest("live/auth0/refresh", _jsonBodyRequest); if (_apiClient.TryRequest(request, out var response)) { if (response.Success) { _tokenExpiresAt = DateTime.UtcNow + _tokenLifetime - OffsetBeforeExpiration; return _tokenCredentials = response; } } Logging.Log.Error($"LeanOAuthTokenHandler.{nameof(GetAccessToken)}: Failed to retrieve access token. Response: {response}. Last known expiry: {_tokenExpiresAt.ToStringInvariant()}."); throw new InvalidOperationException($"Authentication failed. " + $"Details: {(response?.Errors?.Count > 0 ? string.Join(",", response.Errors) : "empty")}"); } catch when (retryCount < MaxRetryCount) { if (cancellationToken.WaitHandle.WaitOne(RetryInterval)) { throw new OperationCanceledException( $"LeanOAuthTokenHandler.{nameof(GetAccessToken)}: Token fetch canceled during wait.", cancellationToken); } } catch (Exception ex) { OnAuthenticationFailed(ex); throw; } } // Unreachable — the loop always returns or throws throw new InvalidOperationException($"LeanOAuthTokenHandler.{nameof(GetAccessToken)}: Unexpected state in token retry loop."); } } } }