This is an automated email from the ASF dual-hosted git repository.

curth pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new e5c25e129 feat(csharp/test/Drivers/Databricks): Support token refresh 
to extend connection lifetime (#3177)
e5c25e129 is described below

commit e5c25e129f99a578b83aa77e9b50a6cdcf4f6765
Author: Alex Guo <[email protected]>
AuthorDate: Mon Jul 21 20:44:00 2025 -0700

    feat(csharp/test/Drivers/Databricks): Support token refresh to extend 
connection lifetime (#3177)
    
    ## Motivation
    
    In scenarios like PowerBI dataset refresh, if a query runs longer than
    the OAuth token's expiration time (typically 1 hour for AAD tokens), the
    connection fails. PowerBI only refreshes access tokens if they have less
    than 20 minutes of expiration time and never updates tokens after a
    connection is opened.
    
    This PR implements token refresh functionality in the Databricks ADBC
    driver using the Databricks token exchange API. When an OAuth token is
    about to expire within a configurable time limit, the driver
    automatically exchanges it for a new token with a longer expiration
    time.
    
    ## Key Components
    
    1. **JWT Token Decoder**: Parses JWT tokens to extract expiration time
    2. **Token Exchange Client**: Handles API calls to the Databricks token
    exchange endpoint
    3. **Token Exchange Handler**: HTTP handler that intercepts requests and
    refreshes tokens when needed
    
    ## Changes
    
    - Added new connection string parameter
    `adbc.databricks.token_renew_limit` to control when token renewal
    happens
    - Implemented JWT token decoding to extract token expiration time
    - Created token exchange client to handle API calls to Databricks token
    exchange endpoint
    - Added HTTP handler to intercept requests and refresh tokens when
    needed
    - Updated connection handling to create and configure the token exchange
    components
    
    ## Testing
    
    - Unit tests for JWT token decoding, token exchange client, and token
    exchange handler
    - End-to-end tests that verify token refresh functionality with real
    tokens
    
    ```
    dotnet test --filter "FullyQualifiedName~JwtTokenDecoderTests"
    dotnet test --filter "FullyQualifiedName~TokenExchangeClientTests"
    dotnet test --filter 
"FullyQualifiedName~TokenExchangeDelegatingHandlerTests"
    ```
---
 .../src/Drivers/Databricks/Auth/JwtTokenDecoder.cs |  96 +++++
 .../Auth/OAuthClientCredentialsProvider.cs         |   1 -
 .../Drivers/Databricks/Auth/TokenExchangeClient.cs | 177 ++++++++
 .../Auth/TokenExchangeDelegatingHandler.cs         | 144 +++++++
 .../src/Drivers/Databricks/DatabricksConnection.cs |  91 ++++-
 .../src/Drivers/Databricks/DatabricksParameters.cs |   6 +
 .../Databricks/E2E/Auth/TokenExchangeTests.cs      | 157 ++++++++
 .../Databricks/E2E/DatabricksTestConfiguration.cs  |   3 +
 .../Databricks/Unit/Auth/JwtTokenDecoderTests.cs   | 124 ++++++
 .../Unit/Auth/TokenExchangeClientTests.cs          | 411 +++++++++++++++++++
 .../Auth/TokenExchangeDelegatingHandlerTests.cs    | 447 +++++++++++++++++++++
 11 files changed, 1638 insertions(+), 19 deletions(-)

diff --git a/csharp/src/Drivers/Databricks/Auth/JwtTokenDecoder.cs 
b/csharp/src/Drivers/Databricks/Auth/JwtTokenDecoder.cs
new file mode 100644
index 000000000..ed68aeeb8
--- /dev/null
+++ b/csharp/src/Drivers/Databricks/Auth/JwtTokenDecoder.cs
@@ -0,0 +1,96 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Text;
+using System.Text.Json;
+
+namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
+{
+    /// <summary>
+    /// Utility class for decoding JWT tokens and extracting claims.
+    /// </summary>
+    internal static class JwtTokenDecoder
+    {
+        /// <summary>
+        /// Tries to parse a JWT token and extract its expiration time.
+        /// </summary>
+        /// <param name="token">The JWT token to parse.</param>
+        /// <param name="expiryTime">The extracted expiration time, if 
successful.</param>
+        /// <returns>True if the expiration time was successfully extracted, 
false otherwise.</returns>
+        public static bool TryGetExpirationTime(string token, out DateTime 
expiryTime)
+        {
+            expiryTime = DateTime.MinValue;
+
+            try
+            {
+                // JWT tokens have three parts separated by dots: 
header.payload.signature
+                string[] parts = token.Split('.');
+                if (parts.Length != 3)
+                {
+                    return false;
+                }
+
+                string payload = DecodeBase64Url(parts[1]);
+
+                using JsonDocument jsonDoc = JsonDocument.Parse(payload);
+
+                if (!jsonDoc.RootElement.TryGetProperty("exp", out JsonElement 
expElement))
+                {
+                    return false;
+                }
+
+                // The exp claim is a Unix timestamp (seconds since epoch)
+                if (!expElement.TryGetInt64(out long expSeconds))
+                {
+                    return false;
+                }
+
+                expiryTime = 
DateTimeOffset.FromUnixTimeSeconds(expSeconds).UtcDateTime;
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        /// <summary>
+        /// Decodes a base64url encoded string to a regular string.
+        /// </summary>
+        /// <param name="base64Url">The base64url encoded string.</param>
+        /// <returns>The decoded string.</returns>
+        private static string DecodeBase64Url(string base64Url)
+        {
+            // Convert base64url to base64
+            string base64 = base64Url
+                .Replace('-', '+')
+                .Replace('_', '/');
+
+            // Add padding if needed
+            switch (base64.Length % 4)
+            {
+                case 2: base64 += "=="; break;
+                case 3: base64 += "="; break;
+            }
+
+            byte[] bytes = Convert.FromBase64String(base64);
+
+            return Encoding.UTF8.GetString(bytes);
+        }
+    }
+}
diff --git 
a/csharp/src/Drivers/Databricks/Auth/OAuthClientCredentialsProvider.cs 
b/csharp/src/Drivers/Databricks/Auth/OAuthClientCredentialsProvider.cs
index a54d6588c..df4fc6d24 100644
--- a/csharp/src/Drivers/Databricks/Auth/OAuthClientCredentialsProvider.cs
+++ b/csharp/src/Drivers/Databricks/Auth/OAuthClientCredentialsProvider.cs
@@ -228,7 +228,6 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
         public void Dispose()
         {
             _tokenLock.Dispose();
-            _httpClient.Dispose();
         }
 
         public string? GetCachedTokenScope()
diff --git a/csharp/src/Drivers/Databricks/Auth/TokenExchangeClient.cs 
b/csharp/src/Drivers/Databricks/Auth/TokenExchangeClient.cs
new file mode 100644
index 000000000..5246c16fa
--- /dev/null
+++ b/csharp/src/Drivers/Databricks/Auth/TokenExchangeClient.cs
@@ -0,0 +1,177 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Collections.Generic;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
+{
+    /// <summary>
+    /// Response from the token exchange API.
+    /// </summary>
+    internal class TokenExchangeResponse
+    {
+        /// <summary>
+        /// The new access token.
+        /// </summary>
+        public string AccessToken { get; set; } = string.Empty;
+
+        /// <summary>
+        /// The token type (e.g., "Bearer").
+        /// </summary>
+        public string TokenType { get; set; } = string.Empty;
+
+        /// <summary>
+        /// The number of seconds until the token expires.
+        /// </summary>
+        public int ExpiresIn { get; set; }
+
+        /// <summary>
+        /// The calculated expiration time based on ExpiresIn.
+        /// </summary>
+        public DateTime ExpiryTime { get; set; }
+    }
+
+    /// <summary>
+    /// Interface for token exchange operations.
+    /// </summary>
+    internal interface ITokenExchangeClient
+    {
+        /// <summary>
+        /// Exchanges the provided token for a new token.
+        /// </summary>
+        /// <param name="token">The token to exchange.</param>
+        /// <param name="cancellationToken">A cancellation token.</param>
+        /// <returns>The response from the token exchange API.</returns>
+        Task<TokenExchangeResponse> ExchangeTokenAsync(string token, 
CancellationToken cancellationToken);
+    }
+
+    /// <summary>
+    /// Client for exchanging tokens using the Databricks token exchange API.
+    /// </summary>
+    internal class TokenExchangeClient : ITokenExchangeClient
+    {
+        private readonly HttpClient _httpClient;
+        private readonly string _tokenExchangeEndpoint;
+
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="TokenExchangeClient"/> class.
+        /// </summary>
+        /// <param name="httpClient">The HTTP client to use for 
requests.</param>
+        /// <param name="host">The host of the Databricks workspace.</param>
+        public TokenExchangeClient(HttpClient httpClient, string host)
+        {
+            _httpClient = httpClient ?? throw new 
ArgumentNullException(nameof(httpClient));
+
+            if (string.IsNullOrEmpty(host))
+            {
+                throw new ArgumentNullException(nameof(host));
+            }
+
+            // Ensure the host doesn't have a trailing slash
+            host = host.TrimEnd('/');
+
+            _tokenExchangeEndpoint = $"https://{host}/oidc/v1/token";;
+        }
+
+        /// <summary>
+        /// Exchanges the provided token for a new token.
+        /// </summary>
+        /// <param name="token">The token to exchange.</param>
+        /// <param name="cancellationToken">A cancellation token.</param>
+        /// <returns>The response from the token exchange API.</returns>
+        public async Task<TokenExchangeResponse> ExchangeTokenAsync(string 
token, CancellationToken cancellationToken)
+        {
+            var content = new FormUrlEncodedContent(new[]
+            {
+                new KeyValuePair<string, string>("grant_type", 
"urn:ietf:params:oauth:grant-type:jwt-bearer"),
+                new KeyValuePair<string, string>("assertion", token)
+            });
+
+            var request = new HttpRequestMessage(HttpMethod.Post, 
_tokenExchangeEndpoint)
+            {
+                Content = content
+            };
+            request.Headers.Accept.Add(new 
System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("*/*"));
+
+            HttpResponseMessage response = await 
_httpClient.SendAsync(request, cancellationToken);
+
+            response.EnsureSuccessStatusCode();
+
+            string responseContent = await 
response.Content.ReadAsStringAsync();
+            return ParseTokenResponse(responseContent);
+        }
+
+        /// <summary>
+        /// Parses the token exchange API response.
+        /// </summary>
+        /// <param name="responseContent">The response content to 
parse.</param>
+        /// <returns>The parsed token exchange response.</returns>
+        private TokenExchangeResponse ParseTokenResponse(string 
responseContent)
+        {
+            using JsonDocument jsonDoc = JsonDocument.Parse(responseContent);
+            var root = jsonDoc.RootElement;
+
+            if (!root.TryGetProperty("access_token", out JsonElement 
accessTokenElement))
+            {
+                throw new DatabricksException("Token exchange response did not 
contain an access_token");
+            }
+
+            string? accessToken = accessTokenElement.GetString();
+            if (string.IsNullOrEmpty(accessToken))
+            {
+                throw new DatabricksException("Token exchange access_token was 
null or empty");
+            }
+
+            if (!root.TryGetProperty("token_type", out JsonElement 
tokenTypeElement))
+            {
+                throw new DatabricksException("Token exchange response did not 
contain token_type");
+            }
+
+            string? tokenType = tokenTypeElement.GetString();
+            if (string.IsNullOrEmpty(tokenType))
+            {
+                throw new DatabricksException("Token exchange token_type was 
null or empty");
+            }
+
+            if (!root.TryGetProperty("expires_in", out JsonElement 
expiresInElement))
+            {
+                throw new DatabricksException("Token exchange response did not 
contain expires_in");
+            }
+
+            int expiresIn = expiresInElement.GetInt32();
+            if (expiresIn <= 0)
+            {
+                throw new DatabricksException("Token exchange expires_in value 
must be positive");
+            }
+
+            DateTime expiryTime = DateTime.UtcNow.AddSeconds(expiresIn);
+
+            return new TokenExchangeResponse
+            {
+                AccessToken = accessToken!,
+                TokenType = tokenType!,
+                ExpiresIn = expiresIn,
+                ExpiryTime = expiryTime
+            };
+        }
+    }
+}
diff --git 
a/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs 
b/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs
new file mode 100644
index 000000000..f24a9a90e
--- /dev/null
+++ b/csharp/src/Drivers/Databricks/Auth/TokenExchangeDelegatingHandler.cs
@@ -0,0 +1,144 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Net.Http;
+using System.Net.Http.Headers;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Apache.Arrow.Adbc.Drivers.Databricks.Auth
+{
+    /// <summary>
+    /// HTTP message handler that automatically refreshes OAuth tokens before 
they expire.
+    /// </summary>
+    internal class TokenExchangeDelegatingHandler : DelegatingHandler
+    {
+        private readonly string _initialToken;
+        private readonly int _tokenRenewLimitMinutes;
+        private readonly SemaphoreSlim _tokenLock = new SemaphoreSlim(1, 1);
+        private readonly ITokenExchangeClient _tokenExchangeClient;
+
+        private string _currentToken;
+        private DateTime _tokenExpiryTime;
+        private bool _tokenExchangeAttempted = false;
+
+        /// <summary>
+        /// Initializes a new instance of the <see 
cref="TokenExchangeDelegatingHandler"/> class.
+        /// </summary>
+        /// <param name="innerHandler">The inner handler to delegate 
to.</param>
+        /// <param name="tokenExchangeClient">The client for token exchange 
operations.</param>
+        /// <param name="initialToken">The initial token from the connection 
string.</param>
+        /// <param name="tokenExpiryTime">The expiry time of the initial 
token.</param>
+        /// <param name="tokenRenewLimitMinutes">The minutes before token 
expiration when we should start renewing the token.</param>
+        public TokenExchangeDelegatingHandler(
+            HttpMessageHandler innerHandler,
+            ITokenExchangeClient tokenExchangeClient,
+            string initialToken,
+            DateTime tokenExpiryTime,
+            int tokenRenewLimitMinutes)
+            : base(innerHandler)
+        {
+            _tokenExchangeClient = tokenExchangeClient ?? throw new 
ArgumentNullException(nameof(tokenExchangeClient));
+            _initialToken = initialToken ?? throw new 
ArgumentNullException(nameof(initialToken));
+            _tokenExpiryTime = tokenExpiryTime;
+            _tokenRenewLimitMinutes = tokenRenewLimitMinutes;
+            _currentToken = initialToken;
+        }
+
+        /// <summary>
+        /// Checks if the token needs to be renewed.
+        /// </summary>
+        /// <returns>True if the token needs to be renewed, false 
otherwise.</returns>
+        private bool NeedsTokenRenewal()
+        {
+            // Only renew if:
+            // 1. We haven't already attempted token exchange (a token can 
only be renewed once)
+            // 2. The token will expire within the renewal limit
+            return !_tokenExchangeAttempted &&
+                   DateTime.UtcNow.AddMinutes(_tokenRenewLimitMinutes) >= 
_tokenExpiryTime;
+        }
+
+        /// <summary>
+        /// Renews the token if needed.
+        /// </summary>
+        /// <param name="cancellationToken">A cancellation token.</param>
+        /// <returns>A task representing the asynchronous operation.</returns>
+        private async Task RenewTokenIfNeededAsync(CancellationToken 
cancellationToken)
+        {
+            if (!NeedsTokenRenewal())
+            {
+                return;
+            }
+
+            // Acquire the lock to ensure only one thread attempts renewal
+            await _tokenLock.WaitAsync(cancellationToken);
+
+            try
+            {
+                // Double-check pattern in case another thread renewed while 
we were waiting
+                if (!NeedsTokenRenewal())
+                {
+                    return;
+                }
+
+                try
+                {
+                    _tokenExchangeAttempted = true;
+
+                    TokenExchangeResponse response = await 
_tokenExchangeClient.ExchangeTokenAsync(_initialToken, cancellationToken);
+
+                    _currentToken = response.AccessToken;
+                    _tokenExpiryTime = response.ExpiryTime;
+                }
+                catch (Exception ex)
+                {
+                    // Log the error but continue with the current token
+                    // This is to avoid interrupting the operation if token 
exchange fails
+                    System.Diagnostics.Debug.WriteLine($"Token exchange 
failed: {ex.Message}");
+                }
+            }
+            finally
+            {
+                _tokenLock.Release();
+            }
+        }
+
+        /// <summary>
+        /// Sends an HTTP request with the current token.
+        /// </summary>
+        /// <param name="request">The HTTP request message to send.</param>
+        /// <param name="cancellationToken">A cancellation token.</param>
+        /// <returns>The HTTP response message.</returns>
+        protected override async Task<HttpResponseMessage> 
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+        {
+            await RenewTokenIfNeededAsync(cancellationToken);
+            request.Headers.Authorization = new 
AuthenticationHeaderValue("Bearer", _currentToken);
+            return await base.SendAsync(request, cancellationToken);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _tokenLock.Dispose();
+            }
+
+            base.Dispose(disposing);
+        }
+    }
+}
diff --git a/csharp/src/Drivers/Databricks/DatabricksConnection.cs 
b/csharp/src/Drivers/Databricks/DatabricksConnection.cs
index 7dee4e612..37263183c 100644
--- a/csharp/src/Drivers/Databricks/DatabricksConnection.cs
+++ b/csharp/src/Drivers/Databricks/DatabricksConnection.cs
@@ -69,6 +69,8 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
         // Default namespace
         private TNamespace? _defaultNamespace;
 
+        private HttpClient? _authHttpClient;
+
         public DatabricksConnection(IReadOnlyDictionary<string, string> 
properties) : base(properties)
         {
             ValidateProperties();
@@ -327,20 +329,26 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
         protected override HttpMessageHandler CreateHttpHandler()
         {
             HttpMessageHandler baseHandler = base.CreateHttpHandler();
+            HttpMessageHandler baseAuthHandler = 
HiveServer2TlsImpl.NewHttpClientHandler(TlsOptions, _proxyConfigurator);
 
             // Add tracing handler to propagate W3C trace context if enabled
             if (_tracePropagationEnabled)
             {
                 baseHandler = new TracingDelegatingHandler(baseHandler, this, 
_traceParentHeaderName, _traceStateEnabled);
+                baseAuthHandler = new 
TracingDelegatingHandler(baseAuthHandler, this, _traceParentHeaderName, 
_traceStateEnabled);
             }
 
             if (TemporarilyUnavailableRetry)
             {
                 // Add retry handler for 503 responses
                 baseHandler = new RetryHttpHandler(baseHandler, 
TemporarilyUnavailableRetryTimeout);
+                baseAuthHandler = new RetryHttpHandler(baseAuthHandler, 
TemporarilyUnavailableRetryTimeout);
             }
 
-            // Add OAuth handler if OAuth authentication is being used
+            Debug.Assert(_authHttpClient == null, "Auth HttpClient should not 
be initialized yet.");
+            _authHttpClient = new HttpClient(baseAuthHandler);
+
+            // Add OAuth client credentials handler if OAuth M2M 
authentication is being used
             if (Properties.TryGetValue(SparkParameters.AuthType, out string? 
authType) &&
                 SparkAuthTypeParser.TryParse(authType, out SparkAuthType 
authTypeValue) &&
                 authTypeValue == SparkAuthType.OAuth &&
@@ -348,28 +356,14 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
                 DatabricksOAuthGrantTypeParser.TryParse(grantTypeStr, out 
DatabricksOAuthGrantType grantType) &&
                 grantType == DatabricksOAuthGrantType.ClientCredentials)
             {
-                // Note: We assume that properties have already been validated
-                if (Properties.TryGetValue(SparkParameters.HostName, out 
string? host) && !string.IsNullOrEmpty(host))
-                {
-                    // Use hostname directly if provided
-                }
-                else if (Properties.TryGetValue(AdbcOptions.Uri, out string? 
uri) && !string.IsNullOrEmpty(uri))
-                {
-                    // Extract hostname from URI if URI is provided
-                    if (Uri.TryCreate(uri, UriKind.Absolute, out Uri? 
parsedUri))
-                    {
-                        host = parsedUri.Host;
-                    }
-                }
+                string host = GetHost();
 
                 Properties.TryGetValue(DatabricksParameters.OAuthClientId, out 
string? clientId);
                 Properties.TryGetValue(DatabricksParameters.OAuthClientSecret, 
out string? clientSecret);
                 Properties.TryGetValue(DatabricksParameters.OAuthScope, out 
string? scope);
 
-                HttpClient OauthHttpClient = new 
HttpClient(HiveServer2TlsImpl.NewHttpClientHandler(TlsOptions, 
_proxyConfigurator));
-
                 var tokenProvider = new OAuthClientCredentialsProvider(
-                    OauthHttpClient,
+                    _authHttpClient,
                     clientId!,
                     clientSecret!,
                     host!,
@@ -377,7 +371,36 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
                     timeoutMinutes: 1
                 );
 
-                return new OAuthDelegatingHandler(baseHandler, tokenProvider);
+                baseHandler = new OAuthDelegatingHandler(baseHandler, 
tokenProvider);
+            }
+            // Add token exchange handler if token renewal is enabled and the 
auth type is OAuth access token
+            else if 
(Properties.TryGetValue(DatabricksParameters.TokenRenewLimit, out string? 
tokenRenewLimitStr) &&
+                int.TryParse(tokenRenewLimitStr, out int tokenRenewLimit) &&
+                tokenRenewLimit > 0 &&
+                Properties.TryGetValue(SparkParameters.AuthType, out string? 
authTypeForToken) &&
+                SparkAuthTypeParser.TryParse(authTypeForToken, out 
SparkAuthType authTypeValueForToken) &&
+                authTypeValueForToken == SparkAuthType.OAuth &&
+                Properties.TryGetValue(SparkParameters.AccessToken, out 
string? accessToken))
+            {
+                if (string.IsNullOrEmpty(accessToken))
+                {
+                    throw new ArgumentException("Access token is required for 
OAuth authentication with token renewal.");
+                }
+
+                // Check if token is a JWT token by trying to decode it
+                if (JwtTokenDecoder.TryGetExpirationTime(accessToken, out 
DateTime expiryTime))
+                {
+                    string host = GetHost();
+
+                    var tokenExchangeClient = new 
TokenExchangeClient(_authHttpClient, host);
+
+                    baseHandler = new TokenExchangeDelegatingHandler(
+                        baseHandler,
+                        tokenExchangeClient,
+                        accessToken,
+                        expiryTime,
+                        tokenRenewLimit);
+                }
             }
 
             return baseHandler;
@@ -673,6 +696,29 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
             }
         }
 
+        /// <summary>
+        /// Gets the host from the connection properties.
+        /// </summary>
+        /// <returns>The host, or empty string if not found.</returns>
+        private string GetHost()
+        {
+            if (Properties.TryGetValue(SparkParameters.HostName, out string? 
host) && !string.IsNullOrEmpty(host))
+            {
+                return host;
+            }
+
+            if (Properties.TryGetValue(AdbcOptions.Uri, out string? uri) && 
!string.IsNullOrEmpty(uri))
+            {
+                // Parse the URI to extract the host
+                if (Uri.TryCreate(uri, UriKind.Absolute, out Uri? parsedUri))
+                {
+                    return parsedUri.Host;
+                }
+            }
+
+            throw new ArgumentException("Host not found in connection 
properties. Please provide a valid host using either 'HostName' or 'Uri' 
property.");
+        }
+
         public override string AssemblyName => s_assemblyName;
 
         public override string AssemblyVersion => s_assemblyVersion;
@@ -685,5 +731,14 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
             }
             return CatalogName;
         }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _authHttpClient?.Dispose();
+            }
+            base.Dispose(disposing);
+        }
     }
 }
diff --git a/csharp/src/Drivers/Databricks/DatabricksParameters.cs 
b/csharp/src/Drivers/Databricks/DatabricksParameters.cs
index 2166fa43f..56030b4e3 100644
--- a/csharp/src/Drivers/Databricks/DatabricksParameters.cs
+++ b/csharp/src/Drivers/Databricks/DatabricksParameters.cs
@@ -206,6 +206,12 @@ namespace Apache.Arrow.Adbc.Drivers.Databricks
         /// When enabled, the driver will also propagate the tracestate header 
if available.
         /// </summary>
         public const string TraceStateEnabled = 
"adbc.databricks.trace_propagation.state_enabled";
+
+        /// <summary>
+        /// The minutes before token expiration when we should start renewing 
the token.
+        /// Default value is 0 (disabled) if not specified.
+        /// </summary>
+        public const string TokenRenewLimit = 
"adbc.databricks.token_renew_limit";
     }
 
     /// <summary>
diff --git a/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs 
b/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs
new file mode 100644
index 000000000..b2e988be1
--- /dev/null
+++ b/csharp/test/Drivers/Databricks/E2E/Auth/TokenExchangeTests.cs
@@ -0,0 +1,157 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Apache.Arrow.Adbc.Drivers.Databricks.Auth;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Auth
+{
+    public class TokenExchangeTests : TestBase<DatabricksTestConfiguration, 
DatabricksTestEnvironment>, IDisposable
+    {
+        private readonly HttpClient _httpClient;
+
+        public TokenExchangeTests(ITestOutputHelper? outputHelper)
+            : base(outputHelper, new DatabricksTestEnvironment.Factory())
+        {
+            _httpClient = new HttpClient();
+        }
+
+        private string GetHost()
+        {
+            string host;
+            if (!string.IsNullOrEmpty(TestConfiguration.HostName))
+            {
+                host = TestConfiguration.HostName;
+            }
+            else if (!string.IsNullOrEmpty(TestConfiguration.Uri))
+            {
+                if (Uri.TryCreate(TestConfiguration.Uri, UriKind.Absolute, out 
Uri? parsedUri))
+                {
+                    host = parsedUri.Host;
+                }
+                else
+                {
+                    throw new ArgumentException($"Invalid URI format: 
{TestConfiguration.Uri}");
+                }
+            }
+            else
+            {
+                throw new ArgumentException("Either HostName or Uri must be 
provided in the test configuration");
+            }
+
+            return host;
+        }
+
+        [SkippableFact]
+        public async Task ExchangeToken_WithValidToken_ReturnsNewToken()
+        {
+            Skip.IfNot(!string.IsNullOrEmpty(TestConfiguration.AccessToken), 
"OAuth access token not configured");
+
+            string host = GetHost();
+            var tokenExchangeClient = new TokenExchangeClient(_httpClient, 
host);
+
+            var response = await 
tokenExchangeClient.ExchangeTokenAsync(TestConfiguration.AccessToken, 
CancellationToken.None);
+
+            Assert.NotNull(response);
+            Assert.NotEmpty(response.AccessToken);
+            Assert.NotEqual(TestConfiguration.AccessToken, 
response.AccessToken);
+            Assert.Equal("Bearer", response.TokenType);
+            Assert.True(response.ExpiresIn > 0);
+            Assert.True(response.ExpiryTime > DateTime.UtcNow);
+        }
+
+        [SkippableFact]
+        public async Task TokenExchangeHandler_WithValidToken_RefreshesToken()
+        {
+            Skip.IfNot(!string.IsNullOrEmpty(TestConfiguration.AccessToken), 
"OAuth access token not configured");
+
+            bool isValidJwt = 
JwtTokenDecoder.TryGetExpirationTime(TestConfiguration.AccessToken, out 
DateTime expiryTime);
+            Skip.IfNot(isValidJwt, "Access token is not a valid JWT token with 
expiration claim");
+
+            // Create a token that's about to expire (by setting expiry time 
to near future)
+            DateTime nearFutureExpiry = DateTime.UtcNow.AddMinutes(5);
+
+            string host = GetHost();
+            var tokenExchangeClient = new TokenExchangeClient(_httpClient, 
host);
+
+            var handler = new TokenExchangeDelegatingHandler(
+                new HttpClientHandler(),
+                tokenExchangeClient,
+                TestConfiguration.AccessToken,
+                nearFutureExpiry,
+                10);
+
+            var httpClient = new HttpClient(handler);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
$"https://{host}/api/2.0/sql/config/warehouses";);
+            var response = await httpClient.SendAsync(request, 
CancellationToken.None);
+
+            // The request should succeed with the refreshed token
+            response.EnsureSuccessStatusCode();
+
+            string content = await response.Content.ReadAsStringAsync();
+            Assert.Contains("sql_configuration_parameters", content);
+        }
+
+        [SkippableFact]
+        public async Task 
TokenExchangeHandler_WithValidTokenNotNearExpiry_UsesOriginalToken()
+        {
+            Skip.IfNot(!string.IsNullOrEmpty(TestConfiguration.AccessToken), 
"OAuth access token not configured");
+
+            bool isValidJwt = 
JwtTokenDecoder.TryGetExpirationTime(TestConfiguration.AccessToken, out 
DateTime expiryTime);
+            Skip.IfNot(isValidJwt, "Access token is not a valid JWT token with 
expiration claim");
+            Skip.If(DateTime.UtcNow.AddMinutes(20) >= expiryTime, "Access 
token is too close to expiration for this test");
+
+            string host = GetHost();
+            var tokenExchangeClient = new TokenExchangeClient(_httpClient, 
host);
+
+            // Create a handler that should not refresh the token (token not 
near expiry)
+            var handler = new TokenExchangeDelegatingHandler(
+                new HttpClientHandler(),
+                tokenExchangeClient,
+                TestConfiguration.AccessToken,
+                expiryTime,
+                10);
+
+            var httpClient = new HttpClient(handler);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
$"https://{host}/api/2.0/sql/config/warehouses";);
+            var response = await httpClient.SendAsync(request, 
CancellationToken.None);
+
+            // The request should succeed with the original token
+            response.EnsureSuccessStatusCode();
+
+            string content = await response.Content.ReadAsStringAsync();
+            Assert.Contains("sql_configuration_parameters", content);
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _httpClient?.Dispose();
+            }
+
+            base.Dispose(disposing);
+        }
+    }
+}
diff --git a/csharp/test/Drivers/Databricks/E2E/DatabricksTestConfiguration.cs 
b/csharp/test/Drivers/Databricks/E2E/DatabricksTestConfiguration.cs
index e4554b113..2b17c6498 100644
--- a/csharp/test/Drivers/Databricks/E2E/DatabricksTestConfiguration.cs
+++ b/csharp/test/Drivers/Databricks/E2E/DatabricksTestConfiguration.cs
@@ -46,6 +46,9 @@ namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks
         [JsonPropertyName("traceStateEnabled"), JsonIgnore(Condition = 
JsonIgnoreCondition.WhenWritingDefault)]
         public string TraceStateEnabled { get; set; } = string.Empty;
 
+        [JsonPropertyName("tokenRenewLimit"), JsonIgnore(Condition = 
JsonIgnoreCondition.WhenWritingDefault)]
+        public string TokenRenewLimit { get; set; } = string.Empty;
+
         [JsonPropertyName("isCITesting"), JsonIgnore(Condition = 
JsonIgnoreCondition.WhenWritingDefault)]
         public bool IsCITesting { get; set; } = false;
     }
diff --git a/csharp/test/Drivers/Databricks/Unit/Auth/JwtTokenDecoderTests.cs 
b/csharp/test/Drivers/Databricks/Unit/Auth/JwtTokenDecoderTests.cs
new file mode 100644
index 000000000..bd630ce49
--- /dev/null
+++ b/csharp/test/Drivers/Databricks/Unit/Auth/JwtTokenDecoderTests.cs
@@ -0,0 +1,124 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Text;
+using System.Text.Json;
+using Apache.Arrow.Adbc.Drivers.Databricks.Auth;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Tests.Drivers.Databricks.Unit.Auth
+{
+    public class JwtTokenDecoderTests
+    {
+        [Fact]
+        public void TryGetExpirationTime_ValidToken_ReturnsTrue()
+        {
+            string token = CreateTestToken(DateTime.UtcNow.AddMinutes(30));
+
+            bool result = JwtTokenDecoder.TryGetExpirationTime(token, out 
DateTime expiryTime);
+
+            Assert.True(result);
+            Assert.True(expiryTime > DateTime.UtcNow);
+            Assert.True(expiryTime < DateTime.UtcNow.AddMinutes(31));
+        }
+
+        [Fact]
+        public void TryGetExpirationTime_ExpiredToken_ReturnsTrue()
+        {
+            string token = CreateTestToken(DateTime.UtcNow.AddMinutes(-30));
+
+            bool result = JwtTokenDecoder.TryGetExpirationTime(token, out 
DateTime expiryTime);
+
+            Assert.True(result);
+            Assert.True(expiryTime < DateTime.UtcNow);
+            Assert.True(expiryTime > DateTime.UtcNow.AddMinutes(-31));
+        }
+
+        [Fact]
+        public void TryGetExpirationTime_InvalidToken_ReturnsFalse()
+        {
+            string token = "invalid.token.format";
+
+            bool result = JwtTokenDecoder.TryGetExpirationTime(token, out 
DateTime expiryTime);
+
+            Assert.False(result);
+            Assert.Equal(DateTime.MinValue, expiryTime);
+        }
+
+        [Fact]
+        public void TryGetExpirationTime_MissingExpClaim_ReturnsFalse()
+        {
+            string token = CreateTestTokenWithoutExpClaim();
+
+            bool result = JwtTokenDecoder.TryGetExpirationTime(token, out 
DateTime expiryTime);
+
+            Assert.False(result);
+            Assert.Equal(DateTime.MinValue, expiryTime);
+        }
+
+        private string CreateTestToken(DateTime expiryTime)
+        {
+            // Create a simple JWT token with expiration claim
+            var header = new { alg = "HS256", typ = "JWT" };
+            var payload = new { exp = 
((DateTimeOffset)expiryTime).ToUnixTimeSeconds() };
+
+            string headerJson = JsonSerializer.Serialize(header);
+            string payloadJson = JsonSerializer.Serialize(payload);
+
+            string headerBase64 = 
Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson))
+                .Replace('+', '-')
+                .Replace('/', '_')
+                .TrimEnd('=');
+
+            string payloadBase64 = 
Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson))
+                .Replace('+', '-')
+                .Replace('/', '_')
+                .TrimEnd('=');
+
+            // For testing purposes, we don't need a valid signature
+            string signature = "signature";
+
+            return $"{headerBase64}.{payloadBase64}.{signature}";
+        }
+
+        private string CreateTestTokenWithoutExpClaim()
+        {
+            // Create a simple JWT token without expiration claim
+            var header = new { alg = "HS256", typ = "JWT" };
+            var payload = new { sub = "test" };
+
+            string headerJson = JsonSerializer.Serialize(header);
+            string payloadJson = JsonSerializer.Serialize(payload);
+
+            string headerBase64 = 
Convert.ToBase64String(Encoding.UTF8.GetBytes(headerJson))
+                .Replace('+', '-')
+                .Replace('/', '_')
+                .TrimEnd('=');
+
+            string payloadBase64 = 
Convert.ToBase64String(Encoding.UTF8.GetBytes(payloadJson))
+                .Replace('+', '-')
+                .Replace('/', '_')
+                .TrimEnd('=');
+
+            // For testing purposes, we don't need a valid signature
+            string signature = "signature";
+
+            return $"{headerBase64}.{payloadBase64}.{signature}";
+        }
+    }
+}
diff --git 
a/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeClientTests.cs 
b/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeClientTests.cs
new file mode 100644
index 000000000..59d99bead
--- /dev/null
+++ b/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeClientTests.cs
@@ -0,0 +1,411 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Apache.Arrow.Adbc.Drivers.Databricks.Auth;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
+{
+    public class TokenExchangeClientTests : IDisposable
+    {
+        private readonly Mock<HttpMessageHandler> _mockHttpMessageHandler;
+        private readonly HttpClient _httpClient;
+        private readonly string _testHost = "test.databricks.com";
+
+        public TokenExchangeClientTests()
+        {
+            _mockHttpMessageHandler = new Mock<HttpMessageHandler>();
+            _httpClient = new HttpClient(_mockHttpMessageHandler.Object);
+        }
+
+        [Fact]
+        public void Constructor_WithValidParameters_SetsEndpointCorrectly()
+        {
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+            Assert.NotNull(client);
+        }
+
+        [Fact]
+        public void Constructor_WithEmptyHost_ThrowsArgumentNullException()
+        {
+            Assert.Throws<ArgumentNullException>(() => new 
TokenExchangeClient(_httpClient, string.Empty));
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithValidResponse_ReturnsTokenExchangeResponse()
+        {
+            var testToken = "test-jwt-token";
+            var expectedAccessToken = "new-access-token";
+            var expectedTokenType = "Bearer";
+            var expectedExpiresIn = 3600;
+
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = expectedAccessToken,
+                token_type = expectedTokenType,
+                expires_in = expectedExpiresIn
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.Is<HttpRequestMessage>(req =>
+                        req.Method == HttpMethod.Post &&
+                        req.RequestUri != null &&
+                        req.RequestUri.ToString() == 
$"https://{_testHost}/oidc/v1/token";),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var result = await client.ExchangeTokenAsync(testToken, 
CancellationToken.None);
+
+            Assert.NotNull(result);
+            Assert.Equal(expectedAccessToken, result.AccessToken);
+            Assert.Equal(expectedTokenType, result.TokenType);
+            Assert.Equal(expectedExpiresIn, result.ExpiresIn);
+            Assert.True(result.ExpiryTime > DateTime.UtcNow);
+            Assert.True(result.ExpiryTime <= 
DateTime.UtcNow.AddSeconds(expectedExpiresIn + 1));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenAsync_SendsCorrectRequestFormat()
+        {
+            var testToken = "test-jwt-token";
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = "token",
+                token_type = "Bearer",
+                expires_in = 3600
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            HttpRequestMessage? capturedRequest = null;
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .Callback<HttpRequestMessage, CancellationToken>((req, ct) => 
capturedRequest = req)
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            await client.ExchangeTokenAsync(testToken, CancellationToken.None);
+
+            Assert.NotNull(capturedRequest);
+            Assert.Equal(HttpMethod.Post, capturedRequest.Method);
+            Assert.Equal($"https://{_testHost}/oidc/v1/token";, 
capturedRequest?.RequestUri?.ToString());
+            Assert.True(capturedRequest?.Headers.Accept.Contains(new 
System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("*/*")));
+
+            var content = capturedRequest?.Content as FormUrlEncodedContent;
+            Assert.NotNull(content);
+
+            var formContent = await content.ReadAsStringAsync();
+            
Assert.Contains("grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer",
 formContent);
+            Assert.Contains($"assertion={testToken}", formContent);
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithHttpError_ThrowsHttpRequestException()
+        {
+            var testToken = "test-jwt-token";
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.Unauthorized)
+            {
+                Content = new StringContent("Unauthorized")
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            await Assert.ThrowsAsync<HttpRequestException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithMissingAccessToken_ThrowsDatabricksException()
+        {
+            var testToken = "test-jwt-token";
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                token_type = "Bearer",
+                expires_in = 3600
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var exception = await Assert.ThrowsAsync<DatabricksException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+
+            Assert.Contains("access_token", exception.Message);
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithEmptyAccessToken_ThrowsDatabricksException()
+        {
+            var testToken = "test-jwt-token";
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = "",
+                token_type = "Bearer",
+                expires_in = 3600
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var exception = await Assert.ThrowsAsync<DatabricksException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+
+            Assert.Contains("access_token was null or empty", 
exception.Message);
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithMissingTokenType_ThrowsDatabricksException()
+        {
+            var testToken = "test-jwt-token";
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = "token",
+                expires_in = 3600
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var exception = await Assert.ThrowsAsync<DatabricksException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+
+            Assert.Contains("token_type", exception.Message);
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithMissingExpiresIn_ThrowsDatabricksException()
+        {
+            var testToken = "test-jwt-token";
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = "token",
+                token_type = "Bearer"
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var exception = await Assert.ThrowsAsync<DatabricksException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+
+            Assert.Contains("expires_in", exception.Message);
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithNegativeExpiresIn_ThrowsDatabricksException()
+        {
+            var testToken = "test-jwt-token";
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = "token",
+                token_type = "Bearer",
+                expires_in = -1
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var exception = await Assert.ThrowsAsync<DatabricksException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+
+            Assert.Contains("expires_in value must be positive", 
exception.Message);
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithInvalidJson_ThrowsJsonException()
+        {
+            var testToken = "test-jwt-token";
+            var invalidJson = "{ invalid json }";
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(invalidJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            await Assert.ThrowsAnyAsync<JsonException>(() =>
+                client.ExchangeTokenAsync(testToken, CancellationToken.None));
+        }
+
+        [Fact]
+        public async Task 
ExchangeTokenAsync_WithCancellationToken_PropagatesCancellation()
+        {
+            var testToken = "test-jwt-token";
+            var cts = new CancellationTokenSource();
+            cts.Cancel();
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ThrowsAsync(new TaskCanceledException());
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            await Assert.ThrowsAsync<TaskCanceledException>(() =>
+                client.ExchangeTokenAsync(testToken, cts.Token));
+        }
+
+        [Fact]
+        public async Task ExchangeTokenAsync_CalculatesExpiryTimeCorrectly()
+        {
+            var testToken = "test-jwt-token";
+            var expiresIn = 1800; // 30 minutes
+            var beforeCall = DateTime.UtcNow;
+
+            var responseJson = JsonSerializer.Serialize(new
+            {
+                access_token = "token",
+                token_type = "Bearer",
+                expires_in = expiresIn
+            });
+
+            var httpResponseMessage = new 
HttpResponseMessage(HttpStatusCode.OK)
+            {
+                Content = new StringContent(responseJson)
+            };
+
+            _mockHttpMessageHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(httpResponseMessage);
+
+            var client = new TokenExchangeClient(_httpClient, _testHost);
+
+            var result = await client.ExchangeTokenAsync(testToken, 
CancellationToken.None);
+            var afterCall = DateTime.UtcNow;
+
+            var expectedMinExpiry = beforeCall.AddSeconds(expiresIn);
+            var expectedMaxExpiry = afterCall.AddSeconds(expiresIn);
+
+            Assert.True(result.ExpiryTime >= expectedMinExpiry);
+            Assert.True(result.ExpiryTime <= expectedMaxExpiry);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _httpClient?.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}
diff --git 
a/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
 
b/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
new file mode 100644
index 000000000..1abf6dd7d
--- /dev/null
+++ 
b/csharp/test/Drivers/Databricks/Unit/Auth/TokenExchangeDelegatingHandlerTests.cs
@@ -0,0 +1,447 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements.  See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Apache.Arrow.Adbc.Drivers.Databricks.Auth;
+using Moq;
+using Moq.Protected;
+using Xunit;
+
+namespace Apache.Arrow.Adbc.Drivers.Databricks.Tests.Auth
+{
+    public class TokenExchangeDelegatingHandlerTests : IDisposable
+    {
+        private readonly Mock<HttpMessageHandler> _mockInnerHandler;
+        private readonly Mock<ITokenExchangeClient> _mockTokenExchangeClient;
+        private readonly string _initialToken = "initial-token";
+        private readonly int _tokenRenewLimitMinutes = 10;
+        private readonly DateTime _initialTokenExpiry = 
DateTime.UtcNow.AddHours(1);
+
+        public TokenExchangeDelegatingHandlerTests()
+        {
+            _mockInnerHandler = new Mock<HttpMessageHandler>();
+            _mockTokenExchangeClient = new Mock<ITokenExchangeClient>();
+        }
+
+        [Fact]
+        public void Constructor_WithValidParameters_InitializesCorrectly()
+        {
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                _initialTokenExpiry,
+                _tokenRenewLimitMinutes);
+
+            Assert.NotNull(handler);
+        }
+
+        [Fact]
+        public void 
Constructor_WithNullTokenExchangeClient_ThrowsArgumentNullException()
+        {
+            Assert.Throws<ArgumentNullException>(() => new 
TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                null!,
+                _initialToken,
+                _initialTokenExpiry,
+                _tokenRenewLimitMinutes));
+        }
+
+        [Fact]
+        public void 
Constructor_WithNullInitialToken_ThrowsArgumentNullException()
+        {
+            Assert.Throws<ArgumentNullException>(() => new 
TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                null!,
+                _initialTokenExpiry,
+                _tokenRenewLimitMinutes));
+        }
+
+        [Fact]
+        public async Task 
SendAsync_WithValidTokenNotNearExpiry_UsesInitialTokenWithoutRenewal()
+        {
+            var futureExpiry = DateTime.UtcNow.AddHours(2); // Well beyond 
renewal limit
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                futureExpiry,
+                _tokenRenewLimitMinutes);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
"https://example.com";);
+            var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK);
+
+            HttpRequestMessage? capturedRequest = null;
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .Callback<HttpRequestMessage, CancellationToken>((req, ct) => 
capturedRequest = req)
+                .ReturnsAsync(expectedResponse);
+
+            var httpClient = new HttpClient(handler);
+            var response = await httpClient.SendAsync(request);
+
+            Assert.Equal(expectedResponse, response);
+            Assert.NotNull(capturedRequest);
+            Assert.Equal("Bearer", 
capturedRequest.Headers.Authorization?.Scheme);
+            Assert.Equal(_initialToken, 
capturedRequest.Headers.Authorization?.Parameter);
+
+            _mockTokenExchangeClient.Verify(
+                x => x.ExchangeTokenAsync(It.IsAny<string>(), 
It.IsAny<CancellationToken>()),
+                Times.Never);
+        }
+
+        [Fact]
+        public async Task 
SendAsync_WithTokenNearExpiry_RenewsTokenBeforeRequest()
+        {
+            // Arrange
+            var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within 
renewal limit
+            var newToken = "new-renewed-token";
+            var newExpiry = DateTime.UtcNow.AddHours(1);
+
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                nearExpiryTime,
+                _tokenRenewLimitMinutes);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
"https://example.com";);
+            var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK);
+
+            var tokenExchangeResponse = new TokenExchangeResponse
+            {
+                AccessToken = newToken,
+                TokenType = "Bearer",
+                ExpiresIn = 3600,
+                ExpiryTime = newExpiry
+            };
+
+            _mockTokenExchangeClient
+                .Setup(x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()))
+                .ReturnsAsync(tokenExchangeResponse);
+
+            HttpRequestMessage? capturedRequest = null;
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .Callback<HttpRequestMessage, CancellationToken>((req, ct) => 
capturedRequest = req)
+                .ReturnsAsync(expectedResponse);
+
+            var httpClient = new HttpClient(handler);
+            var response = await httpClient.SendAsync(request);
+
+            Assert.Equal(expectedResponse, response);
+            Assert.NotNull(capturedRequest);
+            Assert.Equal("Bearer", 
capturedRequest.Headers.Authorization?.Scheme);
+            Assert.Equal(newToken, 
capturedRequest.Headers.Authorization?.Parameter);
+
+            _mockTokenExchangeClient.Verify(
+                x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public async Task 
SendAsync_WithTokenExchangeFailure_ContinuesWithOriginalToken()
+        {
+            var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within 
renewal limit
+
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                nearExpiryTime,
+                _tokenRenewLimitMinutes);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
"https://example.com";);
+            var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK);
+
+            // Setup token exchange to fail
+            _mockTokenExchangeClient
+                .Setup(x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()))
+                .ThrowsAsync(new Exception("Token exchange failed"));
+
+            HttpRequestMessage? capturedRequest = null;
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .Callback<HttpRequestMessage, CancellationToken>((req, ct) => 
capturedRequest = req)
+                .ReturnsAsync(expectedResponse);
+
+            var httpClient = new HttpClient(handler);
+            var response = await httpClient.SendAsync(request);
+
+            Assert.Equal(expectedResponse, response);
+            Assert.NotNull(capturedRequest);
+            Assert.Equal("Bearer", 
capturedRequest.Headers.Authorization?.Scheme);
+            Assert.Equal(_initialToken, 
capturedRequest.Headers.Authorization?.Parameter); // Should still use original 
token
+
+            // Verify token exchange was attempted
+            _mockTokenExchangeClient.Verify(
+                x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public async Task SendAsync_WithRenewedToken_DoesNotRenewAgain()
+        {
+            var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within 
renewal limit
+            var newToken = "new-renewed-token";
+            var newExpiry = DateTime.UtcNow.AddMinutes(3); // New token also 
near expiry
+
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                nearExpiryTime,
+                _tokenRenewLimitMinutes);
+
+            var tokenExchangeResponse = new TokenExchangeResponse
+            {
+                AccessToken = newToken,
+                TokenType = "Bearer",
+                ExpiresIn = 180,
+                ExpiryTime = newExpiry
+            };
+
+            _mockTokenExchangeClient
+                .Setup(x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()))
+                .ReturnsAsync(tokenExchangeResponse);
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+            var httpClient = new HttpClient(handler);
+
+            // Make two requests
+            await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, 
"https://example.com/1";));
+            await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, 
"https://example.com/2";));
+
+            // Token exchange should only be called once (renewed tokens 
cannot be renewed again)
+            _mockTokenExchangeClient.Verify(
+                x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public async Task 
SendAsync_WithConcurrentRequests_OnlyRenewsTokenOnce()
+        {
+            var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within 
renewal limit
+            var newToken = "new-renewed-token";
+            var newExpiry = DateTime.UtcNow.AddHours(1);
+
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                nearExpiryTime,
+                _tokenRenewLimitMinutes);
+
+            var tokenExchangeResponse = new TokenExchangeResponse
+            {
+                AccessToken = newToken,
+                TokenType = "Bearer",
+                ExpiresIn = 3600,
+                ExpiryTime = newExpiry
+            };
+
+            // Add a small delay to token exchange to simulate concurrent 
access
+            _mockTokenExchangeClient
+                .Setup(x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()))
+                .Returns(async () =>
+                {
+                    await Task.Delay(100);
+                    return tokenExchangeResponse;
+                });
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+            var httpClient = new HttpClient(handler);
+
+            // Make concurrent requests
+            var tasks = new[]
+            {
+                httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, 
"https://example.com/1";)),
+                httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, 
"https://example.com/2";)),
+                httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, 
"https://example.com/3";))
+            };
+
+            await Task.WhenAll(tasks);
+
+            // Token exchange should only be called once despite concurrent 
requests
+            _mockTokenExchangeClient.Verify(
+                x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public async Task 
SendAsync_WithCancellationToken_PropagatesCancellation()
+        {
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                _initialTokenExpiry,
+                _tokenRenewLimitMinutes);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
"https://example.com";);
+            var cts = new CancellationTokenSource();
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .Returns<HttpRequestMessage, CancellationToken>((req, ct) =>
+                {
+                    ct.ThrowIfCancellationRequested();
+                    return Task.FromResult(new 
HttpResponseMessage(HttpStatusCode.OK));
+                });
+
+            cts.Cancel();
+            var httpClient = new HttpClient(handler);
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
+                httpClient.SendAsync(request, cts.Token));
+        }
+
+        [Fact]
+        public async Task 
SendAsync_WithTokenRenewalAndCancellation_PropagatesCancellation()
+        {
+            var nearExpiryTime = DateTime.UtcNow.AddMinutes(5); // Within 
renewal limit
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                nearExpiryTime,
+                _tokenRenewLimitMinutes);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
"https://example.com";);
+            var cts = new CancellationTokenSource();
+
+            _mockTokenExchangeClient
+                .Setup(x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()))
+                .Returns<string, CancellationToken>((token, ct) =>
+                {
+                    ct.ThrowIfCancellationRequested();
+                    return Task.FromResult(new TokenExchangeResponse
+                    {
+                        AccessToken = "new-token",
+                        TokenType = "Bearer",
+                        ExpiresIn = 3600,
+                        ExpiryTime = DateTime.UtcNow.AddHours(1)
+                    });
+                });
+
+            cts.Cancel();
+            var httpClient = new HttpClient(handler);
+            await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
+                httpClient.SendAsync(request, cts.Token));
+        }
+
+        [Fact]
+        public void Dispose_ReleasesResources()
+        {
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                _initialTokenExpiry,
+                _tokenRenewLimitMinutes);
+
+            handler.Dispose();
+            handler.Dispose(); // Should be safe to call multiple times
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(5)]
+        [InlineData(15)]
+        public async Task 
SendAsync_WithDifferentRenewalLimits_RenewsTokenAppropriately(int 
renewalLimitMinutes)
+        {
+            var tokenExpiryTime = 
DateTime.UtcNow.AddMinutes(renewalLimitMinutes / 2); // Half the renewal limit
+            var handler = new TokenExchangeDelegatingHandler(
+                _mockInnerHandler.Object,
+                _mockTokenExchangeClient.Object,
+                _initialToken,
+                tokenExpiryTime,
+                renewalLimitMinutes);
+
+            var request = new HttpRequestMessage(HttpMethod.Get, 
"https://example.com";);
+
+            _mockTokenExchangeClient
+                .Setup(x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()))
+                .ReturnsAsync(new TokenExchangeResponse
+                {
+                    AccessToken = "new-token",
+                    TokenType = "Bearer",
+                    ExpiresIn = 3600,
+                    ExpiryTime = DateTime.UtcNow.AddHours(1)
+                });
+
+            _mockInnerHandler.Protected()
+                .Setup<Task<HttpResponseMessage>>(
+                    "SendAsync",
+                    ItExpr.IsAny<HttpRequestMessage>(),
+                    ItExpr.IsAny<CancellationToken>())
+                .ReturnsAsync(new HttpResponseMessage(HttpStatusCode.OK));
+
+            var httpClient = new HttpClient(handler);
+            await httpClient.SendAsync(request);
+
+            _mockTokenExchangeClient.Verify(
+                x => x.ExchangeTokenAsync(_initialToken, 
It.IsAny<CancellationToken>()),
+                Times.Once);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _mockInnerHandler?.Object?.Dispose();
+            }
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}

Reply via email to