Skip to content
17 changes: 1 addition & 16 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,22 +94,7 @@ jobs:
- name: Run manual build steps
if: matrix.build-mode == 'manual'
shell: bash
# TODO(https://sqlclientdrivers.visualstudio.com/ADO.Net/_workitems/edit/41729):
# For some reason, the StressTests projects fail to restore in this
# environment, so we skip them by explicitly building all of the other
# projects instead of the main solution file.
run: |
dotnet build src/Microsoft.SqlServer.Server
dotnet build src/Microsoft.Data.SqlClient/netcore/ref
dotnet build src/Microsoft.Data.SqlClient/netcore/src
dotnet build src/Microsoft.Data.SqlClient/netfx/ref
dotnet build src/Microsoft.Data.SqlClient/netfx/src
dotnet build src/Microsoft.Data.SqlClient/src
dotnet build src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider
dotnet build src/Microsoft.Data.SqlClient/tests/UnitTests
dotnet build src/Microsoft.Data.SqlClient/tests/FunctionalTests
dotnet build src/Microsoft.Data.SqlClient/tests/ManualTests
dotnet build src/Microsoft.Data.SqlClient/tests/PerformanceTests
run: dotnet build src/Microsoft.Data.SqlClient.sln
Comment thread
paulmedynski marked this conversation as resolved.

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ public override void BeforeUnload(SqlAuthenticationMethod authentication)
}

#if NETFRAMEWORK
private Func<System.Windows.Forms.IWin32Window> _iWin32WindowFunc = null;
private Func<System.Windows.Forms.IWin32Window>? _iWin32WindowFunc = null;
Comment thread
paulmedynski marked this conversation as resolved.

/// <include file='../doc/ActiveDirectoryAuthenticationProvider.xml' path='docs/members[@name="ActiveDirectoryAuthenticationProvider"]/SetIWin32WindowFunc/*'/>
public void SetIWin32WindowFunc(Func<System.Windows.Forms.IWin32Window> iWin32WindowFunc) => this._iWin32WindowFunc = iWin32WindowFunc;
public void SetIWin32WindowFunc(Func<System.Windows.Forms.IWin32Window> iWin32WindowFunc) => _iWin32WindowFunc = iWin32WindowFunc;
#endif

/// <include file='../doc/ActiveDirectoryAuthenticationProvider.xml' path='docs/members[@name="ActiveDirectoryAuthenticationProvider"]/AcquireTokenAsync/*'/>
Expand Down Expand Up @@ -607,7 +607,7 @@ public Task<Uri> AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirec

private async Task<IPublicClientApplication> GetPublicClientAppInstanceAsync(PublicClientAppKey publicClientAppKey, CancellationToken cancellationToken)
{
if (!s_pcaMap.TryGetValue(publicClientAppKey, out IPublicClientApplication clientApplicationInstance))
if (!s_pcaMap.TryGetValue(publicClientAppKey, out IPublicClientApplication? clientApplicationInstance))
{
await s_pcaMapModifierSemaphore.WaitAsync(cancellationToken);
try
Expand All @@ -631,7 +631,7 @@ private async Task<IPublicClientApplication> GetPublicClientAppInstanceAsync(Pub
private static async Task<AccessToken> GetTokenAsync(TokenCredentialKey tokenCredentialKey, string secret,
TokenRequestContext tokenRequestContext, CancellationToken cancellationToken)
{
if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out TokenCredentialData tokenCredentialInstance))
if (!s_tokenCredentialMap.TryGetValue(tokenCredentialKey, out TokenCredentialData? tokenCredentialInstance))
{
await s_tokenCredentialMapModifierSemaphore.WaitAsync(cancellationToken);
try
Expand Down Expand Up @@ -704,17 +704,30 @@ private IPublicClientApplication CreateClientAppInstance(PublicClientAppKey publ
PublicClientApplicationBuilder builder = PublicClientApplicationBuilder
.CreateWithApplicationOptions(new PublicClientApplicationOptions
{
ClientId = publicClientAppKey._applicationClientId,
ClientId = publicClientAppKey.ApplicationClientId,
ClientName = typeof(ActiveDirectoryAuthenticationProvider).FullName,
ClientVersion = Extensions.Azure.ThisAssembly.InformationalVersion,
RedirectUri = publicClientAppKey._redirectUri,
RedirectUri = publicClientAppKey.RedirectUri,
})
.WithAuthority(publicClientAppKey._authority);
// The Authority contains the tenant-specific Azure AD endpoint, e.g.
Comment thread
paulmedynski marked this conversation as resolved.
// "https://login.microsoftonline.com/72f988bf-...". The tenant ID is not determined by
// the client; it originates from the SQL Server FEDAUTHINFO TDS token that the server
// sends during the login handshake. The flow is:
//
// 1. TdsParser.TryProcessFedAuthInfo parses the FEDAUTHINFO token and extracts the
// STSURL (authority with tenant) and SPN (resource).
// 2. SqlConnectionInternal passes the STSURL as the 'authority' parameter when
// constructing SqlAuthenticationParametersBuilder.
// 3. AcquireTokenAsync stores the full authority (including tenant) in
// PublicClientAppKey.Authority.
// 4. Here, WithAuthority directs MSAL to authenticate against the correct Azure AD
// tenant.
.WithAuthority(publicClientAppKey.Authority);

#if NETFRAMEWORK
if (_iWin32WindowFunc is not null)
if (publicClientAppKey.IWin32WindowFunc is not null)
{
builder.WithParentActivityOrWindow(_iWin32WindowFunc);
builder.WithParentActivityOrWindow(publicClientAppKey.IWin32WindowFunc);
}
#endif

Expand Down Expand Up @@ -795,45 +808,51 @@ private static TokenCredentialData CreateTokenCredentialInstance(TokenCredential

internal class PublicClientAppKey
{
public readonly string _authority;
public readonly string _redirectUri;
public readonly string _applicationClientId;
public string Authority { get; }
Comment thread
paulmedynski marked this conversation as resolved.
public string RedirectUri { get; }
public string ApplicationClientId { get; }
#if NETFRAMEWORK
public readonly Func<System.Windows.Forms.IWin32Window> _iWin32WindowFunc;
public Func<System.Windows.Forms.IWin32Window>? IWin32WindowFunc { get; }
#endif

public PublicClientAppKey(string authority, string redirectUri, string applicationClientId
#if NETFRAMEWORK
, Func<System.Windows.Forms.IWin32Window> iWin32WindowFunc
#endif
)
public PublicClientAppKey(
string authority,
string redirectUri,
string applicationClientId
#if NETFRAMEWORK
, Func<System.Windows.Forms.IWin32Window>? iWin32WindowFunc
#endif
)
{
_authority = authority;
_redirectUri = redirectUri;
_applicationClientId = applicationClientId;
Authority = authority;
RedirectUri = redirectUri;
ApplicationClientId = applicationClientId;
#if NETFRAMEWORK
_iWin32WindowFunc = iWin32WindowFunc;
IWin32WindowFunc = iWin32WindowFunc;
#endif
}

public override bool Equals(object obj)
public override bool Equals(object? obj)
{
if (obj != null && obj is PublicClientAppKey pcaKey)
{
return (string.CompareOrdinal(_authority, pcaKey._authority) == 0
&& string.CompareOrdinal(_redirectUri, pcaKey._redirectUri) == 0
&& string.CompareOrdinal(_applicationClientId, pcaKey._applicationClientId) == 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
&& IWin32WindowFunc == pcaKey.IWin32WindowFunc
#endif
);
}
return false;
}

public override int GetHashCode() => Tuple.Create(_authority, _redirectUri, _applicationClientId
#if NETFRAMEWORK
, _iWin32WindowFunc
public override int GetHashCode() => Tuple.Create(
Authority,
RedirectUri,
ApplicationClientId
#if NETFRAMEWORK
, IWin32WindowFunc
#endif
).GetHashCode();
}
Expand Down Expand Up @@ -867,7 +886,7 @@ public TokenCredentialKey(Type tokenCredentialType, string authority, string sco
_clientId = clientId;
}

public override bool Equals(object obj)
public override bool Equals(object? obj)
{
if (obj != null && obj is TokenCredentialKey tcKey)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

<!-- Target Config -->
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TargetFrameworks>net462;net8.0</TargetFrameworks>
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
</PropertyGroup>

<!-- Strong name signing ============================================= -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this
// file to you under the MIT license. See the LICENSE file in the project root for more
// information.

using Xunit.Abstractions;

namespace Microsoft.Data.SqlClient.Extensions.Azure.Test;

public class ActiveDirectoryInteractiveTests
{
private readonly ITestOutputHelper _output;

public ActiveDirectoryInteractiveTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
[Trait("Category", "Interactive")]
Comment thread
paulmedynski marked this conversation as resolved.
Comment thread
paulmedynski marked this conversation as resolved.
public async Task TestConnection()
{
SqlConnectionStringBuilder builder = new()
{
// This is an Azure SQL database accessible via MSFT-AzVPN.
//
// To successfully login, you must add your EntraID identity to the database using
// commands like this:
//
// use [Northwind];
// create user [<you>@microsoft.com] from external provider;
// alter role db_datareader add member [paulmedynski@microsoft.com];
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
//
// You must connect to the database as an admin in order run these commands, for example
// as the testodbc@microsoft.com user via SQL username/password authentication.
//
DataSource = "adotest.database.windows.net",
InitialCatalog = "Northwind",
Encrypt = true,
TrustServerCertificate = false,
ConnectTimeout = 180,
Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive
};

var connection = new SqlConnection(builder.ConnectionString);

try
{
connection.Open();
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
Comment thread
paulmedynski marked this conversation as resolved.
Outdated
}
catch (SqlException ex)
{
_output.WriteLine($"Exception: {ex}");

// SqlException doesn't emit all of its errors via its ToString(), so we must do that
// ourselves if we want to see everything.
//
// SqlErrorCollection doesn't support foreach, so we have to iterate by hand to get
// properly typed SqlError instances.
//
for (int i = 0; i < ex.Errors.Count; i++)
{
_output.WriteLine($"Error[{i}]: {ex.Errors[i].ToString()}");
}

// Re-throw to fail the test.
throw;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<AssemblyName>Microsoft.Data.SqlClient.Extensions.Azure.Test</AssemblyName>

<!--
Exclude Interactive tests by default since they require user interaction (e.g. browser
sign-in).

To run them explicitly:

dotnet test -filter "Category=Interactive"

Note that the "filter" argument actually requires two dashes, but that character sequence
isn't permitted within an XML comment, so we only show a single dash above.
-->
<VSTestTestCaseFilter>Category!=Interactive</VSTestTestCaseFilter>
Comment thread
paulmedynski marked this conversation as resolved.
Comment thread
paulmedynski marked this conversation as resolved.
</PropertyGroup>

<ItemGroup>
Expand Down
Loading