diff --git a/.github/prompts/doc-comments.prompt.md b/.github/prompts/doc-comments.prompt.md new file mode 100644 index 0000000000..060f5fea0e --- /dev/null +++ b/.github/prompts/doc-comments.prompt.md @@ -0,0 +1,51 @@ +--- +name: doc-comments +description: Generate XML documentation comments for C# code following .NET best practices. +argument-hint: +agent: agent +tools: ['edit/editFiles', 'read/readFile'] +--- + +You are an expert .NET developer and technical writer. Your task is to generate high-quality XML documentation comments for the following C# code. + +${input:code} + +Follow these best practices and guidelines derived from standard .NET documentation conventions: + +### 1. Standard XML Tags +- **``**: Provide a clear, concise description of the type or member. Start with a verb in the third person (e.g., "Gets", "Sets", "Initializes", "Calculates", "Determines"). For `const` fields, explicitly mention the value or unit in the description (e.g., "The cache expiration time (2 hours)."). +- **``**: Describe each parameter, including its purpose and any specific constraints (e.g., "cannot be null"). +- **``**: Describe the return value for non-void methods. +- **``**: Document specific exceptions that the method is known to throw, especially those validation-related (like `ArgumentNullException`). +- **``**: Use this for property descriptions to describe the value stored in the property. +- **``**: Use for additional details, implementation notes, or complex usage scenarios that don't fit in the summary. +- **``**: Use this tag when the member overrides a base member or implements an interface member and the documentation should be inherited. + +### 2. Formatting and References +- **Code References**: Use `` or `` to reference other types or members within the documentation. +- **Keywords**: Use `` for C# keywords (e.g., ``, ``, ``, ``). +- **Inline Code**: Use `` tags for literal values or short inline code snippets (e.g., `0`). +- **Paragraphs**: Use `` tags to separate paragraphs within `` or `` for readability. + +### 3. Writing Style +- **Focus on Intent**: Do not start summaries with "A helper class...", "A wrapper for...", or "An instance of...". Instead, describe the specific role or responsibility of the type (e.g., "Uniquely identifies a client application configuration..." instead of "A key class..."). +- **Completeness**: Use complete sentences ending with a period. +- **Properties**: + - For `get; set;` properties: "Gets or sets..." + - For `get;` properties: "Gets..." + - Boolean properties: "Gets a value indicating whether..." +- **Constructors**: "Initializes a new instance of the class." +- **Avoid Content-Free Comments**: Do not simply repeat the name of the member (e.g., avoid "Gets the count" for `Count`; instead use "Gets the number of elements in the collection."). + +### 4. Analysis +- **Exceptions**: Analyze the method body to identify thrown exceptions and document them using `` tags. +- **Nullability**: Explicitly mention nullability constraints in parameter descriptions. + +### 5. Repository Constraints +- **Public APIs**: Do **not** generate inline XML documentation comments for `public` members of `public` types. These are documented via external XML files using `` tags. +- **Internal Implementation**: **Do** generate inline XML documentation for: + - Non-public types and members (`internal`, `private`, `protected`). + - `public` members within non-public types (e.g. a `public` method inside an `internal` class). + +**Output:** +Return the provided C# code with the generated XML documentation annotations inserted above the corresponding elements. Maintain existing indentation and code structure. diff --git a/.github/prompts/release-notes.prompt.md b/.github/prompts/release-notes.prompt.md index b2079143b8..eb208be315 100644 --- a/.github/prompts/release-notes.prompt.md +++ b/.github/prompts/release-notes.prompt.md @@ -3,7 +3,7 @@ name: release-notes description: Generate release notes for a specific milestone of the Microsoft.Data.SqlClient project. argument-hint: agent: agent -tools: ['github/search_issues', 'createFile', 'editFiles', 'readFile'] +tools: ['github/search_issues', 'edit/createFile', 'edit/editFiles', 'read/readFile'] --- Generate release notes for the milestone "${input:milestone}". diff --git a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs index e5413110ae..7655e1e59b 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Abstractions/src/SqlAuthenticationMethod.cs @@ -15,7 +15,7 @@ public enum SqlAuthenticationMethod : int /// [Obsolete("ActiveDirectoryPassword is deprecated, use a more secure authentication method. See https://aka.ms/SqlClientEntraIDAuthentication for more details.")] - // Obsoleted with MDS 7.0.0; to be removed at least 2 major versions later. + // Obsolete as of MDS 7.0.0; to be removed at least 2 major versions later. ActiveDirectoryPassword, /// diff --git a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs index ac40d8fdea..eff8a48faf 100644 --- a/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient.Extensions/Azure/src/ActiveDirectoryAuthenticationProvider.cs @@ -23,22 +23,73 @@ public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationPro /// to avoid interactive authentication request every-time, within application scope making use of MSAL's userTokenCache. /// private static readonly ConcurrentDictionary s_pcaMap = new(); + + /// + /// Maps token credential keys to their corresponding token credential data to allow reuse of credentials. + /// private static readonly ConcurrentDictionary s_tokenCredentialMap = new(); + + /// + /// Controls concurrent access to the to ensure thread safety during modifications. + /// private static SemaphoreSlim s_pcaMapModifierSemaphore = new(1, 1); + + /// + /// Controls concurrent access to the to ensure thread safety during modifications. + /// private static SemaphoreSlim s_tokenCredentialMapModifierSemaphore = new(1, 1); - private static readonly MemoryCache s_accountPwCache = new MemoryCache(new MemoryCacheOptions()); + + /// + /// Stores validation hashes for account passwords to verify cache validity. + /// + private static readonly MemoryCache s_accountPwCache = new MemoryCache(new MemoryCacheOptions()); + + /// + /// The time-to-live in hours for items in the account password cache (2 hours). + /// private const int s_accountPwCacheTtlInHours = 2; + + /// + /// The default redirect URI for native clients ("https://login.microsoftonline.com/common/oauth2/nativeclient"). + /// private const string s_nativeClientRedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"; + + /// + /// The suffix appended to the resource to form the default scope ("/.default"). + /// private const string s_defaultScopeSuffix = "/.default"; + + /// + /// The name of the type, used for logging purposes. + /// private readonly string _type = typeof(ActiveDirectoryAuthenticationProvider).Name; + + /// + /// The logger instance used to trace events and errors within the provider. + /// private readonly SqlClientLogger _logger = new(); + + /// + /// The callback delegate invoked to handle the device code flow authentication step. + /// private Func _deviceCodeFlowCallback; + + /// + /// The custom web UI implementation used for interactive authentication when a browser is required. + /// private ICustomWebUi? _customWebUI = null; - private readonly string _applicationClientId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; - // The MSAL error code that indicates the action should be retried. - // - // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/retry-after#simple-retry-for-errors-with-http-error-codes-500-600 + /// + /// The client ID of the application registration in Azure Active Directory (SQL Client default). + /// + private readonly string _applicationClientId = "2fd908ad-0664-4344-b9be-cd3e8b574c38"; + + /// + /// The MSAL error code that indicates the action should be retried (429). + /// + /// + /// See . + /// private const int MsalRetryStatusCode = 429; /// @@ -119,7 +170,7 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication) #endif /// - public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { try { @@ -130,7 +181,7 @@ public override async Task AcquireTokenAsync(SqlAuthenti if (parameters.ConnectionTimeout > 0) { // Safely convert to milliseconds. - if (int.MaxValue / 1000 > parameters.ConnectionTimeout) + if (parameters.ConnectionTimeout > int.MaxValue / 1000) { cts.CancelAfter(int.MaxValue); } @@ -144,216 +195,94 @@ public override async Task AcquireTokenAsync(SqlAuthenti string[] scopes = [scope]; TokenRequestContext tokenRequestContext = new(scopes); - // We split audience from Authority URL here. Audience can be one of - // the following: - // - // - The Azure AD authority audience enumeration - // - The tenant ID, which can be: - // - A GUID (the ID of your Azure AD instance), for - // single-tenant applications - // - A domain name associated with your Azure AD instance (also - // for single-tenant applications) - // - One of these placeholders as a tenant ID in place of the - // Azure AD authority audience enumeration: - // - `organizations` for a multitenant application - // - `consumers` to sign in users only with their personal - // accounts - // - `common` to sign in users with their work and school - // accounts or their personal Microsoft accounts - // - // MSAL will throw a meaningful exception if you specify both the - // Azure AD authority audience and the tenant ID. - // - // If you don't specify an audience, your app will target Azure AD - // and personal Microsoft accounts as an audience. (That is, it - // will behave as though `common` were specified.) - // - // More information: - // - // https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration - - int separatorIndex = parameters.Authority.LastIndexOf('/'); - string authority = parameters.Authority.Remove(separatorIndex + 1); - string audience = parameters.Authority.Substring(separatorIndex + 1); + var (authorityUrl, audience) = SplitAuthority(parameters.Authority); string? clientId = string.IsNullOrWhiteSpace(parameters.UserId) ? null : parameters.UserId; - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDefault) - { - // Cache DefaultAzureCredenial based on scope, authority, audience, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authority, scope, audience, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Default auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - TokenCredentialOptions tokenCredentialOptions = new() { AuthorityHost = new Uri(authority) }; - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryManagedIdentity || parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryMSI) - { - // Cache ManagedIdentityCredential based on scope, authority, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authority, scope, string.Empty, clientId); - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Managed Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal) - { - // Cache ClientSecretCredential based on scope, authority, audience, and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authority, scope, audience, clientId); - string password = parameters.Password is null ? string.Empty : parameters.Password; - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, password, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Service Principal auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity) - { - // Cache WorkloadIdentityCredential based on authority and clientId - TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authority, string.Empty, string.Empty, clientId); - // If either tenant id, client id, or the token file path are not specified when fetching the token, - // a CredentialUnavailableException will be thrown instead - AccessToken accessToken = await GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Workload Identity auth mode. Expiry Time: {0}", accessToken.ExpiresOn); - return new SqlAuthenticationToken(accessToken.Token, accessToken.ExpiresOn); - } - - /* - * Today, MSAL.NET uses another redirect URI by default in desktop applications that run on Windows - * (urn:ietf:wg:oauth:2.0:oob). In the future, we'll want to change this default, so we recommend - * that you use https://login.microsoftonline.com/common/oauth2/nativeclient. - * - * https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration#redirect-uris - */ - string redirectUri = s_nativeClientRedirectUri; - - #if NET - if (parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) + switch (parameters.AuthenticationMethod) { - redirectUri = "http://localhost"; - } - #endif - - PublicClientAppKey pcaKey = - #if NETFRAMEWORK - new(parameters.Authority, redirectUri, _applicationClientId, _iWin32WindowFunc); - #else - new(parameters.Authority, redirectUri, _applicationClientId); - #endif - - AuthenticationResult? result = null; - IPublicClientApplication app = await GetPublicClientAppInstanceAsync(pcaKey, cts.Token).ConfigureAwait(false); - - if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) - { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - - if (result == null) + case SqlAuthenticationMethod.ActiveDirectoryDefault: { - // The AcquireTokenByIntegratedWindowsAuth method is marked - // as obsolete in MSAL.NET but it is still a supported way - // to acquire tokens for Active Directory Integrated - // authentication. - var builder = - #pragma warning disable CS0618 // Type or member is obsolete - app.AcquireTokenByIntegratedWindowsAuth(scopes) - #pragma warning restore CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId); - - if (!string.IsNullOrEmpty(parameters.UserId)) - { - builder = builder.WithUsername(parameters.UserId); - } - - result = await builder - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); + // Cache DefaultAzureCredenial based on scope, authority, audience, and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(DefaultAzureCredential), authorityUrl, scope, audience, clientId); + return GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token); + } - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Integrated auth mode. Expiry Time: {0}", result?.ExpiresOn); + case SqlAuthenticationMethod.ActiveDirectoryManagedIdentity: + case SqlAuthenticationMethod.ActiveDirectoryMSI: + { + // Cache ManagedIdentityCredential based on scope, authority, and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(ManagedIdentityCredential), authorityUrl, scope, string.Empty, clientId); + return GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token); } - } - #pragma warning disable CS0618 // Type or member is obsolete - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryPassword) - #pragma warning restore CS0618 // Type or member is obsolete - { - string pwCacheKey = GetAccountPwCacheKey(parameters); - object? previousPw = s_accountPwCache.Get(pwCacheKey); - string password = parameters.Password is null ? string.Empty : parameters.Password; - byte[] currPwHash = GetHash(password); - - if (previousPw != null && - previousPw is byte[] previousPwBytes && - // Only get the cached token if the current password hash matches the previously used password hash - AreEqual(currPwHash, previousPwBytes)) + + case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + // Cache ClientSecretCredential based on scope, authority, audience, and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(ClientSecretCredential), authorityUrl, scope, audience, clientId); + string password = parameters.Password is null ? string.Empty : parameters.Password; + return GetTokenAsync(tokenCredentialKey, password, tokenRequestContext, cts.Token); } - if (result == null) + case SqlAuthenticationMethod.ActiveDirectoryWorkloadIdentity: { - #pragma warning disable CS0618 // Type or member is obsolete - result = await app.AcquireTokenByUsernamePassword(scopes, parameters.UserId, parameters.Password) - #pragma warning disable CS0618 // Type or member is obsolete - .WithCorrelationId(parameters.ConnectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); - - // We cache the password hash to ensure future connection requests include a validated password - // when we check for a cached MSAL account. Otherwise, a connection request with the same username - // against the same tenant could succeed with an invalid password when we re-use the cached token. - using (ICacheEntry entry = s_accountPwCache.CreateEntry(pwCacheKey)) - { - entry.Value = currPwHash; - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(s_accountPwCacheTtlInHours); - } + // Cache WorkloadIdentityCredential based on authority and clientId + TokenCredentialKey tokenCredentialKey = new(typeof(WorkloadIdentityCredential), authorityUrl, string.Empty, string.Empty, clientId); + // If either tenant id, client id, or the token file path are not specified when fetching the token, + // a CredentialUnavailableException will be thrown instead + return GetTokenAsync(tokenCredentialKey, string.Empty, tokenRequestContext, cts.Token); + } - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Password auth mode. Expiry Time: {0}", result?.ExpiresOn); + case SqlAuthenticationMethod.ActiveDirectoryIntegrated: + { + return AcquireTokenIntegratedAsync( + parameters, + _applicationClientId, + scopes, + cts); } - } - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive || - parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) - { - try + #pragma warning disable CS0618 // Type or member is obsolete + case SqlAuthenticationMethod.ActiveDirectoryPassword: + #pragma warning restore CS0618 // Type or member is obsolete { - result = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); + return AcquireTokenByUsernamePasswordAsync( + parameters, + _applicationClientId, + scopes, + cts); } - catch (MsalUiRequiredException) + case SqlAuthenticationMethod.ActiveDirectoryInteractive: { - // An 'MsalUiRequiredException' is thrown in the case where an interaction is required with the end user of the application, - // for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), - // or the user needs to perform two factor authentication. - // - // result should be null here, but we make sure of that. - Debug.Assert(result is null); - result = null; + return AcquireTokenInteractiveAsync( + parameters, + _applicationClientId, + scopes, + parameters.ConnectionId, + parameters.UserId, + parameters.AuthenticationMethod, + cts, + _customWebUI, + _deviceCodeFlowCallback); } - - if (result == null) + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: { - // If no existing 'account' is found, we request user to sign in interactively. - result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod, cts, _customWebUI, _deviceCodeFlowCallback).ConfigureAwait(false); - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); + return AcquireTokenDeviceFlowAsync( + parameters, + _applicationClientId, + scopes, + parameters.ConnectionId, + parameters.AuthenticationMethod, + cts, + _deviceCodeFlowCallback); + } + default: + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | {0} authentication mode not supported by ActiveDirectoryAuthenticationProvider class.", parameters.AuthenticationMethod); + + throw new Extensions.Azure.AuthenticationException( + parameters.AuthenticationMethod, + $"Authentication method {parameters.AuthenticationMethod} not supported."); } } - else - { - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | {0} authentication mode not supported by ActiveDirectoryAuthenticationProvider class.", parameters.AuthenticationMethod); - - throw new Extensions.Azure.AuthenticationException( - parameters.AuthenticationMethod, - $"Authentication method {parameters.AuthenticationMethod} not supported."); - } - - // TODO: Existing bug? result may be null here. - if (result is null) - { - throw new Extensions.Azure.AuthenticationException( - parameters.AuthenticationMethod, - "Internal error - authentication result is null"); - } - - return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); } catch (MsalException ex) { @@ -438,13 +367,114 @@ AuthenticationRequiredException or } } - private static async Task TryAcquireTokenSilent(IPublicClientApplication app, SqlAuthenticationParameters parameters, - string[] scopes, CancellationTokenSource cts) + /// + /// Splits the authority URL into the authority and audience components. + /// + /// + /// We split audience from Authority URL here. Audience can be one of + /// the following: + /// - The Azure AD authority audience enumeration + /// - The tenant ID, which can be: + /// - A GUID (the ID of your Azure AD instance), for + /// single-tenant applications + /// - A domain name associated with your Azure AD instance (also + /// for single-tenant applications) + /// - One of these placeholders as a tenant ID in place of the + /// Azure AD authority audience enumeration: + /// - `organizations` for a multitenant application + /// - `consumers` to sign in users only with their personal + /// accounts + /// - `common` to sign in users with their work and school + /// accounts or their personal Microsoft accounts + /// + /// MSAL will throw a meaningful exception if you specify both the + /// Azure AD authority audience and the tenant ID. + /// + /// If you don't specify an audience, your app will target Azure AD + /// and personal Microsoft accounts as an audience. (That is, it + /// will behave as though `common` were specified.) + /// + /// More information: + /// https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration + /// + /// The authority URL to split. + /// A tuple containing the authority and audience. + private (string authorityUrl, string audience) SplitAuthority(string authority) + { + int separatorIndex = authority.LastIndexOf('/'); + string authorityUrl = authority.Remove(separatorIndex + 1); + string audience = authority.Substring(separatorIndex + 1); + return (authorityUrl, audience); + } + + /// + /// Acquires an access token using the username and password authentication flow. + /// + /// The authentication parameters containing user credentials. + /// The client ID of the application. + /// The scopes to request the token for. + /// The cancellation token source to control the operation cancellation. + /// A task that returns the acquired . + private static async Task AcquireTokenByUsernamePasswordAsync( + SqlAuthenticationParameters parameters, + string applicationClientId, + string[] scopes, + CancellationTokenSource cts) { - AuthenticationResult? result = null; + IPublicClientApplication app = await GetPublicClientAppInstanceAsync(parameters, applicationClientId, cts.Token).ConfigureAwait(false); + + string pwCacheKey = GetAccountPwCacheKey(parameters); + object? previousPw = s_accountPwCache.Get(pwCacheKey); + string password = parameters.Password is null ? string.Empty : parameters.Password; + byte[] currPwHash = GetHash(password); + + if (previousPw is not null && + previousPw is byte[] previousPwBytes && + // Only get the cached token if the current password hash matches the previously used password hash + AreEqual(currPwHash, previousPwBytes)) + { + SqlAuthenticationToken? cachedResult = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + + if (cachedResult is not null) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, cachedResult.ExpiresOn); + return cachedResult; + } + } + + #pragma warning disable CS0618 // Type or member is obsolete + AuthenticationResult result = await app.AcquireTokenByUsernamePassword(scopes, parameters.UserId, parameters.Password) + #pragma warning restore CS0618 // Type or member is obsolete + .WithCorrelationId(parameters.ConnectionId) + .ExecuteAsync(cancellationToken: cts.Token) + .ConfigureAwait(false); + + // We cache the password hash to ensure future connection requests include a validated password + // when we check for a cached MSAL account. Otherwise, a connection request with the same username + // against the same tenant could succeed with an invalid password when we re-use the cached token. + using (ICacheEntry entry = s_accountPwCache.CreateEntry(pwCacheKey)) + { + entry.Value = currPwHash; + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(s_accountPwCacheTtlInHours); + } + + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token for Active Directory Password auth mode. Expiry Time: {0}", result.ExpiresOn); + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } + /// + /// Attempts to acquire an access token silently using the cached account in the public client application. + /// + /// The public client application instance. + /// The authentication parameters. + /// The scopes to request the token for. + /// The cancellation token source to control the operation cancellation. + /// A task that returns the acquired if successful; otherwise, . + private static async Task TryAcquireTokenSilent(IPublicClientApplication app, SqlAuthenticationParameters parameters, + string[] scopes, CancellationTokenSource cts) + { // Fetch available accounts from 'app' instance - System.Collections.Generic.IEnumerator accounts = (await app.GetAccountsAsync().ConfigureAwait(false)).GetEnumerator(); + IEnumerator accounts = (await app.GetAccountsAsync().ConfigureAwait(false)).GetEnumerator(); IAccount? account = default; if (accounts.MoveNext()) @@ -472,84 +502,216 @@ AuthenticationRequiredException or { // If 'account' is available in 'app', we use the same to acquire token silently. // Read More on API docs: https://docs.microsoft.com/dotnet/api/microsoft.identity.client.clientapplicationbase.acquiretokensilent - result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken: cts.Token).ConfigureAwait(false); + AuthenticationResult? result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken: cts.Token).ConfigureAwait(false); SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result?.ExpiresOn); + return result != null ? new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn) : null; + } + + return null; + } + + /// + /// Acquires an access token using Integrated Windows Authentication. + /// + /// The authentication parameters. + /// The client ID of the application. + /// The scopes to request the token for. + /// The cancellation token source to control the operation cancellation. + /// A task that returns the acquired . + private static async Task AcquireTokenIntegratedAsync( + SqlAuthenticationParameters parameters, + string applicationClientId, + string[] scopes, + CancellationTokenSource cts) + { + IPublicClientApplication app = await GetPublicClientAppInstanceAsync(parameters, applicationClientId, cts.Token).ConfigureAwait(false); + + SqlAuthenticationToken? cachedResult = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + + if (cachedResult is not null) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, cachedResult.ExpiresOn); + return cachedResult; } - return result; + // The AcquireTokenByIntegratedWindowsAuth method is marked + // as obsolete in MSAL.NET but it is still a supported way + // to acquire tokens for Active Directory Integrated + // authentication. + #pragma warning disable CS0618 // Type or member is obsolete + var builder = app.AcquireTokenByIntegratedWindowsAuth(scopes) + #pragma warning restore CS0618 // Type or member is obsolete + .WithCorrelationId(parameters.ConnectionId); + + if (!string.IsNullOrEmpty(parameters.UserId)) + { + builder = builder.WithUsername(parameters.UserId); + } + + AuthenticationResult result = await builder + .ExecuteAsync(cancellationToken: cts.Token) + .ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result.ExpiresOn); + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); } - private static async Task AcquireTokenInteractiveDeviceFlowAsync(IPublicClientApplication app, string[] scopes, Guid connectionId, string? userId, - SqlAuthenticationMethod authenticationMethod, CancellationTokenSource cts, ICustomWebUi? customWebUI, Func deviceCodeFlowCallback) + /// + /// Acquires an access token using the device code flow. + /// + /// The authentication parameters. + /// The client ID of the application. + /// The scopes to request the token for. + /// The connection ID associated with the request. + /// The authentication method being used. + /// The cancellation token source to control the operation cancellation. + /// The callback to handle device code display. + /// A task that returns the acquired . + private static async Task AcquireTokenDeviceFlowAsync( + SqlAuthenticationParameters parameters, + string applicationClientId, + string[] scopes, + Guid connectionId, + SqlAuthenticationMethod authenticationMethod, + CancellationTokenSource cts, + Func deviceCodeFlowCallback) { + IPublicClientApplication app = await GetPublicClientAppInstanceAsync(parameters, applicationClientId, cts.Token).ConfigureAwait(false); + try { - if (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) + SqlAuthenticationToken? cachedResult = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + + if (cachedResult is not null) { - CancellationTokenSource ctsInteractive = new(); - #if NET - // On .NET Core, MSAL will start the system browser as a - // separate process. MSAL does not have control over this - // browser, but once the user finishes authentication, the web - // page is redirected in such a way that MSAL can intercept the - // Uri. MSAL cannot detect if the user navigates away or simply - // closes the browser. Apps using this technique are encouraged - // to define a timeout (via CancellationToken). We recommend a - // timeout of at least a few minutes, to take into account cases - // where the user is prompted to change password or perform 2FA. - // - // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core#system-browser-experience - // - // Wait up to 3 minutes. - ctsInteractive.CancelAfter(180000); - #endif - if (customWebUI != null) - { - return await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(connectionId) - .WithCustomWebUi(customWebUI) - .WithLoginHint(userId) - .ExecuteAsync(ctsInteractive.Token) - .ConfigureAwait(false); - } - else - { - /* - * We will use the MSAL Embedded or System web browser which changes by Default in MSAL according to this table: - * - * Framework Embedded System Default - * ------------------------------------------- - * .NET Classic Yes Yes^ Embedded - * .NET Core No Yes^ System - * .NET Standard No No NONE - * UWP Yes No Embedded - * Xamarin.Android Yes Yes System - * Xamarin.iOS Yes Yes System - * Xamarin.Mac Yes No Embedded - * - * ^ Requires "http://localhost" redirect URI - * - * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser#at-a-glance - */ - return await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(connectionId) - .WithLoginHint(userId) - .ExecuteAsync(ctsInteractive.Token) - .ConfigureAwait(false); - } + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, cachedResult .ExpiresOn); + return cachedResult; } - else + } + catch (MsalUiRequiredException) + { + // An 'MsalUiRequiredException' is thrown in the case where an interaction is required with the end user of the application, + // for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), + // or the user needs to perform two factor authentication. + } + + try + { + AuthenticationResult result = await app.AcquireTokenWithDeviceCode(scopes, + deviceCodeResult => deviceCodeFlowCallback(deviceCodeResult)) + .WithCorrelationId(connectionId) + .ExecuteAsync(cancellationToken: cts.Token) + .ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result.ExpiresOn); + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } + catch (OperationCanceledException ex) + { + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Operation timed out while acquiring access token."); + + throw new Extensions.Azure.AuthenticationException( + authenticationMethod, + "OperationCanceled", + false, + 0, + // TODO: This used to use the following localized strings + // Strings.SQL_Timeout_Active_Directory_DeviceFlow_Authentication + ex.Message, + ex); + } + } + + /// + /// Acquires an access token interactively, typically showing a login prompt. + /// + /// The authentication parameters. + /// The client ID of the application. + /// The scopes to request the token for. + /// The connection ID associated with the request. + /// The user ID hint. + /// The authentication method being used. + /// The cancellation token source to control the operation cancellation. + /// The custom web UI to use for interaction. + /// The callback for device code flow if fallback is needed (though not directly used here). + /// A task that returns the acquired . + private static async Task AcquireTokenInteractiveAsync( + SqlAuthenticationParameters parameters, + string applicationClientId, + string[] scopes, + Guid connectionId, + string? userId, + SqlAuthenticationMethod authenticationMethod, + CancellationTokenSource cts, + ICustomWebUi? customWebUI, + Func deviceCodeFlowCallback) + { + IPublicClientApplication app = await GetPublicClientAppInstanceAsync(parameters, applicationClientId, cts.Token).ConfigureAwait(false); + + try + { + SqlAuthenticationToken? cachedResult = await TryAcquireTokenSilent(app, parameters, scopes, cts).ConfigureAwait(false); + + if (cachedResult is not null) { - return await app.AcquireTokenWithDeviceCode(scopes, - deviceCodeResult => deviceCodeFlowCallback(deviceCodeResult)) - .WithCorrelationId(connectionId) - .ExecuteAsync(cancellationToken: cts.Token) - .ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (silent) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, cachedResult .ExpiresOn); + return cachedResult; } } + catch (MsalUiRequiredException) + { + // An 'MsalUiRequiredException' is thrown in the case where an interaction is required with the end user of the application, + // for instance, if no refresh token was in the cache, or the user needs to consent, or re-sign-in (for instance if the password expired), + // or the user needs to perform two factor authentication. + } + + try + { + CancellationTokenSource ctsInteractive = new(); + #if NET + // On .NET Core, MSAL will start the system browser as a + // separate process. MSAL does not have control over this + // browser, but once the user finishes authentication, the web + // page is redirected in such a way that MSAL can intercept the + // Uri. MSAL cannot detect if the user navigates away or simply + // closes the browser. Apps using this technique are encouraged + // to define a timeout (via CancellationToken). We recommend a + // timeout of at least a few minutes, to take into account cases + // where the user is prompted to change password or perform 2FA. + // + // https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-Core#system-browser-experience + // + // Wait up to 3 minutes. + ctsInteractive.CancelAfter(180000); + #endif + + /* + * We will use the MSAL Embedded or System web browser which changes by Default in MSAL according to this table: + * + * Framework Embedded System Default + * ------------------------------------------- + * .NET Classic Yes Yes^ Embedded + * .NET Core No Yes^ System + * .NET Standard No No NONE + * UWP Yes No Embedded + * Xamarin.Android Yes Yes System + * Xamarin.iOS Yes Yes System + * Xamarin.Mac Yes No Embedded + * + * ^ Requires "http://localhost" redirect URI + * + * https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/MSAL.NET-uses-web-browser#at-a-glance + */ + AuthenticationResult result = await app.AcquireTokenInteractive(scopes) + .WithCorrelationId(connectionId) + .WithCustomWebUi(customWebUI) + .WithLoginHint(userId) + .ExecuteAsync(ctsInteractive.Token) + .ConfigureAwait(false); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenAsync | Acquired access token (interactive) for {0} auth mode. Expiry Time: {1}", parameters.AuthenticationMethod, result.ExpiresOn); + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } catch (OperationCanceledException ex) { - SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveDeviceFlowAsync | Operation timed out while acquiring access token."); + SqlClientEventSource.Log.TryTraceEvent("AcquireTokenInteractiveAsync | Operation timed out while acquiring access token."); throw new Extensions.Azure.AuthenticationException( authenticationMethod, @@ -566,6 +728,11 @@ private static async Task AcquireTokenInteractiveDeviceFlo } } + /// + /// The default callback method for device code flow that prints the message to the console. + /// + /// The device code result containing the message and code. + /// A completed task. private static Task DefaultDeviceFlowCallback(DeviceCodeResult result) { // This will print the message on the console which tells the user where to go sign-in using @@ -583,28 +750,68 @@ private static Task DefaultDeviceFlowCallback(DeviceCodeResult result) return Task.FromResult(0); } + /// + /// A custom web UI implementation that delegates the authorization code acquisition to a callback. + /// private class CustomWebUi : ICustomWebUi { private readonly Func> _acquireAuthorizationCodeAsyncCallback; + /// + /// Initializes a new instance of the class. + /// + /// The callback to invoke for acquiring the authorization code. internal CustomWebUi(Func> acquireAuthorizationCodeAsyncCallback) => _acquireAuthorizationCodeAsyncCallback = acquireAuthorizationCodeAsyncCallback; + /// public Task AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) => _acquireAuthorizationCodeAsyncCallback.Invoke(authorizationUri, redirectUri, cancellationToken); } - private async Task GetPublicClientAppInstanceAsync(PublicClientAppKey publicClientAppKey, CancellationToken cancellationToken) + /// + /// Gets or creates a public client application instance based on the provided parameters. + /// + /// The authentication parameters. + /// The application client ID. + /// The cancellation token. + /// A task that returns an instance. + private static async Task GetPublicClientAppInstanceAsync( + SqlAuthenticationParameters parameters, + string applicationClientId, + CancellationToken cancellationToken) { - if (!s_pcaMap.TryGetValue(publicClientAppKey, out IPublicClientApplication clientApplicationInstance)) + /* + * Today, MSAL.NET uses another redirect URI by default in desktop applications that run on Windows + * (urn:ietf:wg:oauth:2.0:oob). In the future, we'll want to change this default, so we recommend + * that you use https://login.microsoftonline.com/common/oauth2/nativeclient. + * + * https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration#redirect-uris + */ + string redirectUri = s_nativeClientRedirectUri; + + #if NET + if (parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) + { + redirectUri = "http://localhost"; + } + #endif + PublicClientAppKey pcaKey = + #if NETFRAMEWORK + new(parameters.Authority, redirectUri, applicationClientId, _iWin32WindowFunc); + #else + new(parameters.Authority, redirectUri, applicationClientId); + #endif + + if (!s_pcaMap.TryGetValue(pcaKey, out IPublicClientApplication clientApplicationInstance)) { await s_pcaMapModifierSemaphore.WaitAsync(cancellationToken); try { // Double-check in case another thread added it while we waited for the semaphore - if (!s_pcaMap.TryGetValue(publicClientAppKey, out clientApplicationInstance)) + if (!s_pcaMap.TryGetValue(pcaKey, out clientApplicationInstance)) { - clientApplicationInstance = CreateClientAppInstance(publicClientAppKey); - s_pcaMap.TryAdd(publicClientAppKey, clientApplicationInstance); + clientApplicationInstance = CreateClientAppInstance(pcaKey); + s_pcaMap.TryAdd(pcaKey, clientApplicationInstance); } } finally @@ -616,7 +823,15 @@ private async Task GetPublicClientAppInstanceAsync(Pub return clientApplicationInstance; } - private static async Task GetTokenAsync(TokenCredentialKey tokenCredentialKey, string secret, + /// + /// Gets a token using the specified token credential key and secret. + /// + /// The key identifying the token credential. + /// The secret associated with the credential (e.g., password or client secret). + /// The context for the token request. + /// The cancellation token. + /// A task that returns the acquired . + private static async Task GetTokenAsync(TokenCredentialKey tokenCredentialKey, string secret, TokenRequestContext tokenRequestContext, CancellationToken cancellationToken) { if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out TokenCredentialData tokenCredentialInstance)) @@ -653,14 +868,26 @@ private static async Task GetTokenAsync(TokenCredentialKey tokenCre } } - return await tokenCredentialInstance._tokenCredential.GetTokenAsync(tokenRequestContext, cancellationToken); + AccessToken result = await tokenCredentialInstance._tokenCredential.GetTokenAsync(tokenRequestContext, cancellationToken); + SqlClientEventSource.Log.TryTraceEvent($"AcquireTokenAsync | Acquired access token for {tokenCredentialKey._tokenCredentialType.Name} auth mode. Expiry Time: {result.ExpiresOn}"); + return new SqlAuthenticationToken(result.Token, result.ExpiresOn); } + /// + /// Generates a cache key for the account password cache. + /// + /// The authentication parameters. + /// A string key used for caching. private static string GetAccountPwCacheKey(SqlAuthenticationParameters parameters) { return parameters.Authority + "+" + parameters.UserId; } + /// + /// Computes the SHA256 hash of the input string. + /// + /// The input string to hash. + /// A byte array containing the hash. private static byte[] GetHash(string input) { byte[] unhashedBytes = Encoding.Unicode.GetBytes(input); @@ -669,6 +896,12 @@ private static byte[] GetHash(string input) return hashedBytes; } + /// + /// Compares two byte arrays for equality. + /// + /// The first byte array. + /// The second byte array. + /// if the arrays are equal; otherwise, . private static bool AreEqual(byte[] a1, byte[] a2) { if (ReferenceEquals(a1, a2)) @@ -687,7 +920,12 @@ private static bool AreEqual(byte[] a1, byte[] a2) return a1.AsSpan().SequenceEqual(a2.AsSpan()); } - private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publicClientAppKey) + /// + /// Creates a new instance of using the provided key. + /// + /// The key containing configuration for the application. + /// A new instance. + private static IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publicClientAppKey) { PublicClientApplicationBuilder builder = PublicClientApplicationBuilder .CreateWithApplicationOptions(new PublicClientApplicationOptions @@ -709,6 +947,12 @@ private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publ return builder.Build(); } + /// + /// Creates a new instance of using the provided key and secret. + /// + /// The key containing configuration for the token credential. + /// The secret to be used with the credential. + /// A new instance. private static TokenCredentialData CreateTokenCredentialInstance(TokenCredentialKey tokenCredentialKey, string secret) { if (tokenCredentialKey._tokenCredentialType == typeof(DefaultAzureCredential)) @@ -724,7 +968,9 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential if (tokenCredentialKey._clientId is not null) { defaultAzureCredentialOptions.ManagedIdentityClientId = tokenCredentialKey._clientId; + #pragma warning disable CS0618 // Type or member is obsolete defaultAzureCredentialOptions.SharedTokenCacheUsername = tokenCredentialKey._clientId; + #pragma warning restore CS0618 // Type or member is obsolete defaultAzureCredentialOptions.WorkloadIdentityClientId = tokenCredentialKey._clientId; } @@ -781,15 +1027,39 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential throw new ArgumentException(nameof(ActiveDirectoryAuthenticationProvider)); } + /// + /// Uniquely identifies a client application configuration for caching purposes. + /// internal class PublicClientAppKey { + /// + /// The authority (e.g., https://login.microsoftonline.com/tenant). + /// public readonly string _authority; + /// + /// The redirect URI. + /// public readonly string _redirectUri; + /// + /// The application client ID. + /// public readonly string _applicationClientId; #if NETFRAMEWORK + /// + /// A function to get the parent window for interactive authentication (wrapper for IWin32Window). + /// public readonly Func _iWin32WindowFunc; #endif + /// + /// Initializes a new instance of the class. + /// + /// The authority URL. + /// The redirect URI. + /// The application client ID. +#if NETFRAMEWORK + /// The function to get the parent window handle. +#endif public PublicClientAppKey(string authority, string redirectUri, string applicationClientId #if NETFRAMEWORK , Func iWin32WindowFunc @@ -804,21 +1074,23 @@ public PublicClientAppKey(string authority, string redirectUri, string applicati #endif } + /// public override bool Equals(object obj) { if (obj != null && obj is PublicClientAppKey pcaKey) { - return (string.CompareOrdinal(_authority, pcaKey._authority) == 0 + return string.CompareOrdinal(_authority, pcaKey._authority) == 0 && string.CompareOrdinal(_redirectUri, pcaKey._redirectUri) == 0 && string.CompareOrdinal(_applicationClientId, pcaKey._applicationClientId) == 0 #if NETFRAMEWORK && pcaKey._iWin32WindowFunc == _iWin32WindowFunc #endif - ); + ; } return false; } + /// public override int GetHashCode() => Tuple.Create(_authority, _redirectUri, _applicationClientId #if NETFRAMEWORK , _iWin32WindowFunc @@ -826,11 +1098,25 @@ public override int GetHashCode() => Tuple.Create(_authority, _redirectUri, _app ).GetHashCode(); } + /// + /// Encapsulates token credential data, including the credential instance and a hash of the secret. + /// internal class TokenCredentialData { + /// + /// The token credential instance. + /// public TokenCredential _tokenCredential; + /// + /// The hash of the secret used to create the credential. + /// public byte[] _secretHash; + /// + /// Initializes a new instance of the class. + /// + /// The token credential. + /// The hash of the secret. public TokenCredentialData(TokenCredential tokenCredential, byte[] secretHash) { _tokenCredential = tokenCredential; @@ -838,14 +1124,40 @@ public TokenCredentialData(TokenCredential tokenCredential, byte[] secretHash) } } + /// + /// Uniquely identifies a token credential configuration for caching purposes. + /// internal class TokenCredentialKey { + /// + /// The type of the token credential. + /// public readonly Type _tokenCredentialType; + /// + /// The authority URL. + /// public readonly string _authority; + /// + /// The scope requested. + /// public readonly string _scope; + /// + /// The audience (tenant ID or similar). + /// public readonly string _audience; + /// + /// The client ID (optional). + /// public readonly string? _clientId; + /// + /// Initializes a new instance of the class. + /// + /// The type of the credential. + /// The authority URL. + /// The scope. + /// The audience. + /// The client ID. public TokenCredentialKey(Type tokenCredentialType, string authority, string scope, string audience, string? clientId) { _tokenCredentialType = tokenCredentialType; @@ -855,6 +1167,7 @@ public TokenCredentialKey(Type tokenCredentialType, string authority, string sco _clientId = clientId; } + /// public override bool Equals(object obj) { if (obj != null && obj is TokenCredentialKey tcKey) @@ -869,13 +1182,23 @@ public override bool Equals(object obj) return false; } + /// public override int GetHashCode() => Tuple.Create(_tokenCredentialType, _authority, _scope, _audience, _clientId).GetHashCode(); } } +/// +/// Provides internal logging capabilities for the SQL Client. +/// internal class SqlClientLogger { + /// + /// Logs an informational message. + /// + /// The type name where the log originated. + /// The method name where the log originated. + /// The message to log. public void LogInfo(string type, string method, string message) { SqlClientEventSource.Log.TryTraceEvent( @@ -883,14 +1206,28 @@ public void LogInfo(string type, string method, string message) } } +/// +/// Wraps the event source for internal tracing. +/// internal class SqlClientEventSource { + /// + /// A simple logger implementation that does nothing (placeholder). + /// internal class Logger { + /// + /// Tries to trace an event (placeholder). + /// + /// The message format string. + /// The arguments for the message. public void TryTraceEvent(string message, params object?[] args) { } } + /// + /// The singleton logger instance. + /// public static readonly Logger Log = new(); }