Skip to content

Custom AAD token provider in Winforms can still deadlock in GetFedAuthInfo  #1630

@shueybubbles

Description

@shueybubbles

Using M.D.S 3.1

Describe the bug

I am using a custom MSAL-based authentication provider (code is way too big to paste here and I've not had time to make a smaller repro app).

The crux of the issue is that using MSAL with WAM/broker and Winforms doesn't work with M.D.S.

This is how we construct the MSAL app and fetch a token:

PublicClientApplicationBuilder.Create(appKey.ClientId)
                                            .WithAuthority(string.Format("{0}/{1}/", appKey.Authority.TrimEnd('/'), AzureAuthenticationConfiguration.TenantWildcard), validateAuthority: false)
                                            .WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient")
                                            .WithWindowsBroker()
                                            .WithWindowsBrokerOptions(new WindowsBrokerOptions()
                                            {
                                                // GetAccounts will return Work and School accounts from Windows
                                                ListWindowsWorkAndSchoolAccounts = true,

                                                // Legacy support for 1st party apps only
                                                MsaPassthrough = true
                                            })
                                            .WithLogging(LoggingCallback, LogLevel.Verbose, enablePiiLogging: true)
                                            .Build();

...

return await clientApp.AcquireTokenInteractive(scopes)
                        .WithPrompt(promptType)
                        // To make MSAL auth pop up as modal of SSMS pop ups.
                        .WithParentActivityOrWindow(windowHandle)
                        .WithTenantId(tenant)
                        .ExecuteAsync();

The UI thread gets here:

                                authParamsBuilder.WithUserId(ConnectionOptions.UserID);
                                fedAuthToken = Task.Run(async () => await authProvider.AcquireTokenAsync(authParamsBuilder)).GetAwaiter().GetResult().ToSqlFedAuthToken();
                                _activeDirectoryAuthTimeoutRetryHelper.CachedToken = fedAuthToken;

>	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.GetFedAuthToken(Microsoft.Data.SqlClient.SqlFedAuthInfo fedAuthInfo) Line 2815	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.OnFedAuthInfo(Microsoft.Data.SqlClient.SqlFedAuthInfo fedAuthInfo) Line 2661	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.TdsParser.TryRun(Microsoft.Data.SqlClient.RunBehavior runBehavior, Microsoft.Data.SqlClient.SqlCommand cmdHandler, Microsoft.Data.SqlClient.SqlDataReader dataStream, Microsoft.Data.SqlClient.BulkCopySimpleResultSet bulkCopyHandler, Microsoft.Data.SqlClient.TdsParserStateObject stateObj, out bool dataReady) Line 2683	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.TdsParser.Run(Microsoft.Data.SqlClient.RunBehavior runBehavior, Microsoft.Data.SqlClient.SqlCommand cmdHandler, Microsoft.Data.SqlClient.SqlDataReader dataStream, Microsoft.Data.SqlClient.BulkCopySimpleResultSet bulkCopyHandler, Microsoft.Data.SqlClient.TdsParserStateObject stateObj) Line 2222	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.CompleteLogin(bool enlistOK) Line 1447	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.AttemptOneLogin(Microsoft.Data.SqlClient.ServerInfo serverInfo, string newPassword, System.Security.SecureString newSecurePassword, bool ignoreSniOpenTimeout, Microsoft.Data.ProviderBase.TimeoutTimer timeout, bool withFailover, bool isFirstTransparentAttempt, bool disableTnir) Line 2320	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.LoginNoFailover(Microsoft.Data.SqlClient.ServerInfo serverInfo, string newPassword, System.Security.SecureString newSecurePassword, bool redirectedUserInstance, Microsoft.Data.SqlClient.SqlConnectionString connectionOptions, Microsoft.Data.SqlClient.SqlCredential credential, Microsoft.Data.ProviderBase.TimeoutTimer timeout) Line 1850	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.OpenLoginEnlist(Microsoft.Data.ProviderBase.TimeoutTimer timeout, Microsoft.Data.SqlClient.SqlConnectionString connectionOptions, Microsoft.Data.SqlClient.SqlCredential credential, string newPassword, System.Security.SecureString newSecurePassword, bool redirectedUserInstance) Line 1692	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlInternalConnectionTds.SqlInternalConnectionTds(Microsoft.Data.ProviderBase.DbConnectionPoolIdentity identity, Microsoft.Data.SqlClient.SqlConnectionString connectionOptions, Microsoft.Data.SqlClient.SqlCredential credential, object providerInfo, string newPassword, System.Security.SecureString newSecurePassword, bool redirectedUserInstance, Microsoft.Data.SqlClient.SqlConnectionString userConnectionOptions, Microsoft.Data.SqlClient.SessionData reconnectSessionData, Microsoft.Data.SqlClient.ServerCertificateValidationCallback serverCallback, Microsoft.Data.SqlClient.ClientCertificateRetrievalCallback clientCallback, Microsoft.Data.ProviderBase.DbConnectionPool pool, string accessToken, Microsoft.Data.SqlClient.SqlClientOriginalNetworkAddressInfo originalNetworkAddressInfo, bool applyTransientFaultHandling) Line 537	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlConnectionFactory.CreateConnection(Microsoft.Data.Common.DbConnectionOptions options, Microsoft.Data.Common.DbConnectionPoolKey poolKey, object poolGroupProviderInfo, Microsoft.Data.ProviderBase.DbConnectionPool pool, System.Data.Common.DbConnection owningConnection, Microsoft.Data.Common.DbConnectionOptions userOptions) Line 145	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionFactory.CreatePooledConnection(Microsoft.Data.ProviderBase.DbConnectionPool pool, System.Data.Common.DbConnection owningObject, Microsoft.Data.Common.DbConnectionOptions options, Microsoft.Data.Common.DbConnectionPoolKey poolKey, Microsoft.Data.Common.DbConnectionOptions userOptions) Line 163	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionPool.CreateObject(System.Data.Common.DbConnection owningObject, Microsoft.Data.Common.DbConnectionOptions userOptions, Microsoft.Data.ProviderBase.DbConnectionInternal oldConnection) Line 853	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionPool.UserCreateRequest(System.Data.Common.DbConnection owningObject, Microsoft.Data.Common.DbConnectionOptions userOptions, Microsoft.Data.ProviderBase.DbConnectionInternal oldConnection) Line 2014	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionPool.TryGetConnection(System.Data.Common.DbConnection owningObject, uint waitForMultipleObjectsTimeout, bool allowCreate, bool onlyOneCheckConnection, Microsoft.Data.Common.DbConnectionOptions userOptions, out Microsoft.Data.ProviderBase.DbConnectionInternal connection) Line 1555	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionPool.TryGetConnection(System.Data.Common.DbConnection owningObject, System.Threading.Tasks.TaskCompletionSource<Microsoft.Data.ProviderBase.DbConnectionInternal> retry, Microsoft.Data.Common.DbConnectionOptions userOptions, out Microsoft.Data.ProviderBase.DbConnectionInternal connection) Line 1302	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionFactory.TryGetConnection(System.Data.Common.DbConnection owningConnection, System.Threading.Tasks.TaskCompletionSource<Microsoft.Data.ProviderBase.DbConnectionInternal> retry, Microsoft.Data.Common.DbConnectionOptions userOptions, Microsoft.Data.ProviderBase.DbConnectionInternal oldConnection, out Microsoft.Data.ProviderBase.DbConnectionInternal connection) Line 354	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.ProviderBase.DbConnectionInternal.TryOpenConnectionInternal(System.Data.Common.DbConnection outerConnection, Microsoft.Data.ProviderBase.DbConnectionFactory connectionFactory, System.Threading.Tasks.TaskCompletionSource<Microsoft.Data.ProviderBase.DbConnectionInternal> retry, Microsoft.Data.Common.DbConnectionOptions userOptions) Line 759	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlConnection.TryOpenInner(System.Threading.Tasks.TaskCompletionSource<Microsoft.Data.ProviderBase.DbConnectionInternal> retry) Line 2134	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlConnection.TryOpen(System.Threading.Tasks.TaskCompletionSource<Microsoft.Data.ProviderBase.DbConnectionInternal> retry, Microsoft.Data.SqlClient.SqlConnectionOverrides overrides) Line 2105	C#
 	Microsoft.Data.SqlClient.dll!Microsoft.Data.SqlClient.SqlConnection.Open(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides) Line 1658	C#
 	MsalTestApp.exe!MsalTestApp.Form1.ConnectButton_Click(object sender, System.EventArgs e) Line 39	C#
 	System.Windows.Forms.dll!System.Windows.Forms.Control.OnClick(System.EventArgs e)	Unknown

Meanwhile the MSAL code sees it's running on an MTA thread instead of a UI thread so it tries to spin up its own splash window:

        private Task<bool> ShowPickerWithSplashScreenAsync()
        {

            if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
            {
                TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

                Thread thread = new Thread(() =>
                {
                    try
                    {
                        ShowPickerWithSplashScreenImpl();
                        tcs.SetResult(true);
                    }
                    catch (Exception e)
                    {
                        tcs.SetException(e);
                    }
                });
                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
                return tcs.Task;
            }
            else
            {
                ShowPickerWithSplashScreenImpl();
                return Task.FromResult(true);
            }
        }

That thread ends up trying to disable windows on the original UI thread because we gave it the application's window handle. That call deadlocks.

 	mscorlib.dll!System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext) Line 243	C#
 	mscorlib.dll!System.Threading.WaitHandle.WaitOne(int millisecondsTimeout, bool exitContext) Line 194	C#
 	System.Windows.Forms.dll!System.Windows.Forms.Control.WaitForWaitHandle(System.Threading.WaitHandle waitHandle)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Control.MarshaledInvoke(System.Windows.Forms.Control caller, System.Delegate method, object[] args, bool synchronous)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Control.Invoke(System.Delegate method, object[] args)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ModalApplicationContext.DisableThreadWindows(bool disable, bool onlyWinForms)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.DisableWindowsForModalLoop(bool onlyWinForms, System.Windows.Forms.ApplicationContext context)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.BeginModalMessageLoop(System.Windows.Forms.ApplicationContext context)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(int reason, System.Windows.Forms.ApplicationContext context)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Application.ThreadContext.RunMessageLoop(int reason, System.Windows.Forms.ApplicationContext context)	Unknown
 	System.Windows.Forms.dll!System.Windows.Forms.Form.ShowDialog(System.Windows.Forms.IWin32Window owner)	Unknown
 	Microsoft.Identity.Client.Desktop.dll!Microsoft.Identity.Client.Platforms.Features.WamBroker.AccountPicker.ShowPickerWithSplashScreenImpl() Line 167	C#
>	Microsoft.Identity.Client.Desktop.dll!Microsoft.Identity.Client.Platforms.Features.WamBroker.AccountPicker.ShowPickerWithSplashScreenAsync.AnonymousMethod__0() Line 130	C#
 	mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 980	C#
 	mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 928	C#
 	mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Line 917	C#
 	mscorlib.dll!System.Threading.ThreadHelper.ThreadStart() Line 111	C#

I feel like SqlConnection.Open needs to capture the synchronization context at the time of the call and make sure it doesn't block it while waiting for the AAD callbacks to finish. Maybe we need some way for the application to hand it some special waiter implementation so winforms apps can just process Windows messages.

This bug would likely block adoption of WAM-based auth for SQL connections in SSMS.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions