public async Task <IAuthenticationResult> GetAsync(
            AzureSqlConnectionStringBuilder connectionString,
            string clientCertificateData,
            ILogger logger = null)
        {
            AccessTokenCacheValue accessToken;

            if (TryGetValue(connectionString, out accessToken) && !IsExpired(accessToken))
            {
                // Refresh access token in background, if near expiry or client certificate has changed.
                if (IsNearExpiry(accessToken) || ClientCertificateHasChanged(accessToken, clientCertificateData))
                {
                    TriggerBackgroundRefresh(connectionString, clientCertificateData, logger);
                }

                // Returned cached access token.
                return(accessToken.AuthenticationResult);
            }

            // Acquire token in foreground, first time or if already expired.
            if (await TryRefreshAccessTokenAsync(connectionString, clientCertificateData, logger, ForegroundRefreshTimeoutMilliseconds))
            {
                if (TryGetValue(connectionString, out accessToken))
                {
                    return(accessToken.AuthenticationResult);
                }
            }

            throw new InvalidOperationException($"Failed to acquire access token for {connectionString.Sql.InitialCatalog}.");
        }
        /// <summary>
        /// Refresh the access token in the token cache.
        /// </summary>
        private async Task <bool> RefreshAccessTokenAsync(
            AzureSqlConnectionStringBuilder connectionString,
            string clientCertificateData,
            ILogger logger)
        {
            try
            {
                var start       = DateTimeOffset.Now;
                var accessToken = await AcquireAccessTokenAsync(connectionString, clientCertificateData);

                Debug.Assert(accessToken != null);

                logger?.LogInformation("Refreshed access token {Token} for catalog {Catalog} which expires in {ExpirationMinutes}m (Latency = {ElapsedMilliseconds}ms).",
                                       accessToken.AuthenticationResult.AccessToken.GetHashCode(),
                                       connectionString.Sql.InitialCatalog,
                                       (accessToken.AuthenticationResult.ExpiresOn - DateTimeOffset.Now).TotalMinutes.ToString("F2"),
                                       (DateTimeOffset.Now - start).TotalMilliseconds.ToString("F2"));

                _cache.AddOrUpdate(connectionString.ConnectionString, accessToken, (k, v) => accessToken);

                return(true);
            }
            catch (Exception ex)
            {
                logger?.LogError(0, ex, "Failed to refresh access token for {Catalog}.",
                                 connectionString.Sql.InitialCatalog);

                return(false);
            }
        }
        /// <summary>
        /// Try to refresh the access token in the token cache.
        /// </summary>
        private async Task <bool> TryRefreshAccessTokenAsync(
            AzureSqlConnectionStringBuilder connectionString,
            string clientCertificateData,
            ILogger logger,
            int refreshTimeoutMilliseconds)
        {
            if (!await AcquireTokenLock.WaitAsync(refreshTimeoutMilliseconds))
            {
                return(false);
            }

            try
            {
                TryGetValue(connectionString, out var accessToken);
                if (accessToken == null || IsNearExpiry(accessToken) ||
                    ClientCertificateHasChanged(accessToken, clientCertificateData))
                {
                    return(await RefreshAccessTokenAsync(connectionString, clientCertificateData, logger));
                }
            }
            finally
            {
                AcquireTokenLock.Release();
            }

            return(true);
        }
        /// <summary>
        /// Refresh the access token in the token cache.
        /// </summary>
        private async Task <bool> RefreshAccessTokenAsync(
            AzureSqlConnectionStringBuilder connectionString,
            string clientCertificateData,
            ILogger logger)
        {
            try
            {
                var start       = DateTimeOffset.Now;
                var accessToken = await AcquireAccessTokenAsync(connectionString, clientCertificateData);

                Debug.Assert(accessToken != null);

                logger?.LogInformation("Refreshed access token for {InitialCatalog} in {ElapsedMilliseconds}.",
                                       connectionString.Sql.InitialCatalog,
                                       (DateTimeOffset.Now - start).TotalMilliseconds);

                _cache.AddOrUpdate(connectionString.ConnectionString, accessToken, (k, v) => accessToken);

                return(true);
            }
            catch (Exception ex)
            {
                logger?.LogError(0, ex, "Failed to refresh access token for {InitialCatalog}.",
                                 connectionString.Sql.InitialCatalog);

                return(false);
            }
        }
Beispiel #5
0
 public AzureSqlConnectionFactory(
     AzureSqlConnectionStringBuilder connectionString,
     ISecretInjector secretInjector,
     ILogger logger = null)
 {
     ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
     SecretInjector   = secretInjector ?? throw new ArgumentNullException(nameof(secretInjector));
     Logger           = logger;
 }
Beispiel #6
0
        public AzureSqlConnectionFactory(string connectionString, ISecretInjector secretInjector, ILogger logger = null)
        {
            if (string.IsNullOrEmpty(connectionString))
            {
                throw new ArgumentException("Value cannot be null or empty", nameof(connectionString));
            }

            ConnectionString = new AzureSqlConnectionStringBuilder(connectionString);
            SecretInjector   = secretInjector ?? throw new ArgumentNullException(nameof(secretInjector));
            Logger           = logger;
        }
 /// <summary>
 /// Attempt non-blocking refresh of access token in background with retries.
 /// </summary>
 private void TriggerBackgroundRefresh(
     AzureSqlConnectionStringBuilder connectionString,
     string clientCertificateData,
     ILogger logger)
 {
     Task.Run(async() =>
     {
         await TryRefreshAccessTokenAsync(connectionString, clientCertificateData, logger,
                                          refreshTimeoutMilliseconds: BackgroundRefreshTimeoutMilliseconds);
     });
 }
        /// <summary>
        /// Request an access token from the token service.
        /// </summary>
        protected virtual async Task <AccessTokenCacheValue> AcquireAccessTokenAsync(
            AzureSqlConnectionStringBuilder connectionString,
            string clientCertificateData)
        {
            using (var certificate = new X509Certificate2(Convert.FromBase64String(clientCertificateData), string.Empty))
            {
                var clientAssertion = new ClientAssertionCertificate(connectionString.AadClientId, certificate);
                var authContext     = new AuthenticationContext(connectionString.AadAuthority, tokenCache: null);

                var authResult = await authContext.AcquireTokenAsync(AzureSqlResourceTokenUrl, clientAssertion, connectionString.AadSendX5c);

                return(new AccessTokenCacheValue(clientCertificateData, authResult));
            }
        }
 protected virtual bool TryGetValue(
     AzureSqlConnectionStringBuilder connectionString,
     out AccessTokenCacheValue accessToken)
 {
     return(_cache.TryGetValue(connectionString.ConnectionString, out accessToken));
 }