diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index 182e9e5fe8..d9497a9bce 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -106,7 +106,7 @@ internal static string ConvertToString(object value) internal static bool TryConvertToAuthenticationType(string value, out SqlAuthenticationMethod result) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 5, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 6, "SqlAuthenticationMethod enum has changed, update needed"); bool isSuccess = false; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8bc0eb8d46..303f14596c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2242,7 +2242,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) } break; default: - throw new InvalidOperationException($"Failed to get a token with unsupported auth method {ConnectionOptions.Authentication}."); + throw SQL.UnsupportedAuthenticationSpecified(ConnectionOptions.Authentication); } Debug.Assert(fedAuthToken.accessToken != null, "AccessToken should not be null."); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs index 191b7039f8..84b1e47fa5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -390,6 +390,11 @@ internal static Exception UnsupportedSqlAuthenticationMethod(SqlAuthenticationMe return ADP.NotSupported(System.SRHelper.GetString(SR.SQL_UnsupportedSqlAuthenticationMethod, authentication)); } + internal static Exception UnsupportedAuthenticationSpecified(SqlAuthenticationMethod authentication) + { + return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_UnsupportedAuthenticationSpecified, authentication)); + } + internal static Exception CannotCreateAuthProvider(string authentication, string type, Exception e) { return ADP.Argument(System.SRHelper.GetString(SR.SQL_CannotCreateAuthProvider, authentication, type), e); @@ -425,6 +430,12 @@ internal static Exception ParameterCannotBeEmpty(string paramName) return ADP.ArgumentNull(System.SRHelper.GetString(SR.SQL_ParameterCannotBeEmpty, paramName)); } + internal static Exception ActiveDirectoryInteractiveTimeout() + { + return ADP.TimeoutException(SR.SQL_Timeout_Active_Directory_Interactive_Authentication); + } + + // // SQL.DataCommand // diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs index c3e62e211d..056cb98dbd 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs @@ -3147,6 +3147,15 @@ internal static string SQL_Timeout { } } + /// + /// Looks up a localized string similar to Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request.. + /// + internal static string SQL_Timeout_Active_Directory_Interactive_Authentication { + get { + return ResourceManager.GetString("SQL_Timeout_Active_Directory_Interactive_Authentication", resourceCulture); + } + } + /// /// Looks up a localized string similar to Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.. /// @@ -3300,6 +3309,15 @@ internal static string SQL_UnsupportedAuthenticationByProvider { } } + /// + /// Looks up a localized string similar to Unsupported authentication specified in this context: {0}. + /// + internal static string SQL_UnsupportedAuthenticationSpecified { + get { + return ResourceManager.GetString("SQL_UnsupportedAuthenticationSpecified", resourceCulture); + } + } + /// /// Looks up a localized string similar to The server is attempting to use a feature that is not supported on this platform.. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx index f4168cd021..930d7ad451 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx @@ -1869,4 +1869,10 @@ Cannot use 'Authentication=Active Directory Integrated', if the Credential property has been set. - \ No newline at end of file + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs index 379014c97a..b0ba92425d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Common/DbConnectionStringCommon.cs @@ -525,7 +525,7 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj internal static bool TryConvertToAuthenticationType(string value, out SqlAuthenticationMethod result) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 5, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 6, "SqlAuthenticationMethod enum has changed, update needed"); bool isSuccess = false; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0443daec52..3024273c1c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2723,7 +2723,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) } break; default: - throw new InvalidOperationException($"Failed to get a token with unsupported auth method {ConnectionOptions.Authentication}."); + throw SQL.UnsupportedAuthenticationSpecified(ConnectionOptions.Authentication); } Debug.Assert(fedAuthToken.accessToken != null, "AccessToken should not be null."); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs index 55b1141f5c..45ed5cd2e3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -421,6 +421,11 @@ static internal Exception UnsupportedSqlAuthenticationMethod(SqlAuthenticationMe return ADP.NotSupported(StringsHelper.GetString(Strings.SQL_UnsupportedSqlAuthenticationMethod, authentication)); } + static internal Exception UnsupportedAuthenticationSpecified(SqlAuthenticationMethod authentication) + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_UnsupportedAuthenticationSpecified, authentication)); + } + static internal Exception CannotCreateAuthProvider(string authentication, string type, Exception e) { return ADP.Argument(StringsHelper.GetString(Strings.SQL_CannotCreateAuthProvider, authentication, type), e); @@ -456,6 +461,11 @@ static internal Exception ParameterCannotBeEmpty(string paramName) return ADP.ArgumentNull(StringsHelper.GetString(Strings.SQL_ParameterCannotBeEmpty, paramName)); } + static internal Exception ActiveDirectoryInteractiveTimeout() + { + return ADP.TimeoutException(Strings.SQL_Timeout_Active_Directory_Interactive_Authentication); + } + // // SQL.DataCommand // diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index 0e5f67f488..40921911b0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -10071,6 +10071,15 @@ internal static string SQL_Timeout { } } + /// + /// Looks up a localized string similar to Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request.. + /// + internal static string SQL_Timeout_Active_Directory_Interactive_Authentication { + get { + return ResourceManager.GetString("SQL_Timeout_Active_Directory_Interactive_Authentication", resourceCulture); + } + } + /// /// Looks up a localized string similar to Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.. /// @@ -10242,6 +10251,15 @@ internal static string SQL_UnsupportedAuthenticationByProvider { } } + /// + /// Looks up a localized string similar to Unsupported authentication specified in this context: {0}. + /// + internal static string SQL_UnsupportedAuthenticationSpecified { + get { + return ResourceManager.GetString("SQL_UnsupportedAuthenticationSpecified", resourceCulture); + } + } + /// /// Looks up a localized string similar to SQL authentication method '{0}' is not supported.. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index d6f8ec6e0b..8a0176ed0d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -4530,4 +4530,10 @@ UDT size must be less than {1}, size: {0} + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs index c80f7aa143..d533fbee58 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs @@ -2,7 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Diagnostics; +using System.Linq; using System.Security; +using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; @@ -44,6 +48,7 @@ public override Task AcquireTokenAsync(SqlAuthentication .WithAuthority(parameters.Authority) .WithClientName(Common.DbConnectionStringDefaults.ApplicationName) .WithClientVersion(Common.ADP.GetAssemblyVersion().ToString()) + .WithRedirectUri("http://localhost") .Build(); if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) @@ -62,17 +67,89 @@ public override Task AcquireTokenAsync(SqlAuthentication .WithCorrelationId(parameters.ConnectionId) .ExecuteAsync().Result; } + else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) + { + var accounts = await app.GetAccountsAsync(); + IAccount account; + if (!string.IsNullOrEmpty(parameters.UserId)) + { + account = accounts.FirstOrDefault(a => parameters.UserId.Equals(a.Username, System.StringComparison.InvariantCultureIgnoreCase)); + } + else + { + account = accounts.FirstOrDefault(); + } + + if (null != account) + { + try + { + result = await app.AcquireTokenSilent(scopes, account).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + result = await AcquireTokenInteractive(app, scopes, parameters.ConnectionId, parameters.UserId); + } + } + else + { + result = await AcquireTokenInteractive(app, scopes, parameters.ConnectionId, parameters.UserId); + } + } else { - result = await app.AcquireTokenInteractive(scopes) - .WithCorrelationId(parameters.ConnectionId) - .WithLoginHint(parameters.UserId) - .ExecuteAsync(); + throw SQL.UnsupportedAuthenticationSpecified(parameters.AuthenticationMethod); } return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); }); + private async Task AcquireTokenInteractive(IPublicClientApplication app, string[] scopes, Guid connectionId, string userId) + { + CancellationTokenSource cts = new CancellationTokenSource(); +#if netcoreapp + /* + * 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 + */ + cts.CancelAfter(180000); +#endif + try + { + return await app.AcquireTokenInteractive(scopes) + /* + * 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 + */ + //.WithUseEmbeddedWebView(true) + .WithCorrelationId(connectionId) + .WithLoginHint(userId) + .ExecuteAsync(cts.Token); + } + catch (OperationCanceledException) + { + throw SQL.ActiveDirectoryInteractiveTimeout(); + } + } + /// /// Checks support for authentication type in lower case. /// Interactive authentication added.