diff --git a/doc/samples/AADAuthenticationCustomDeviceFlowCallback.cs b/doc/samples/AADAuthenticationCustomDeviceFlowCallback.cs new file mode 100644 index 0000000000..bda3554bbe --- /dev/null +++ b/doc/samples/AADAuthenticationCustomDeviceFlowCallback.cs @@ -0,0 +1,30 @@ +// +using System; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Data.SqlClient; + +namespace CustomAuthenticationProviderExamples +{ + public class Program + { + public static void Main() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(CustomDeviceFlowCallback); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + using (SqlConnection sqlConnection = new SqlConnection("Server=.database.windows.net;Authentication=Active Directory Device Code Flow;Database=;")) + { + sqlConnection.Open(); + Console.WriteLine("Connected successfully!"); + } + } + + private static Task CustomDeviceFlowCallback(DeviceCodeResult result) + { + // Provide custom logic to process result information and read device code. + Console.WriteLine(result.Message); + return Task.FromResult(0); + } + } +} +// diff --git a/doc/samples/CustomDeviceCodeFlowAzureAuthenticationProvider.cs b/doc/samples/CustomDeviceCodeFlowAzureAuthenticationProvider.cs new file mode 100644 index 0000000000..19bc1f849d --- /dev/null +++ b/doc/samples/CustomDeviceCodeFlowAzureAuthenticationProvider.cs @@ -0,0 +1,56 @@ +// +using System; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Data.SqlClient; + +namespace CustomAuthenticationProviderExamples +{ + /// + /// Example demonstrating creating a custom device code flow authentication provider and attaching it to the driver. + /// This is helpful for applications that wish to override the Callback for the Device Code Result implemented by the SqlClient driver. + /// + public class CustomDeviceCodeFlowAzureAuthenticationProvider : SqlAuthenticationProvider + { + public override async Task AcquireTokenAsync(SqlAuthenticationParameters parameters) + { + string clientId = "my-client-id"; + string clientName = "My Application Name"; + string s_defaultScopeSuffix = "/.default"; + + string[] scopes = new string[] { parameters.Resource.EndsWith(s_defaultScopeSuffix) ? parameters.Resource : parameters.Resource + s_defaultScopeSuffix }; + + IPublicClientApplication app = PublicClientApplicationBuilder.Create(clientId) + .WithAuthority(parameters.Authority) + .WithClientName(clientName) + .WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient") + .Build(); + + AuthenticationResult result = await app.AcquireTokenWithDeviceCode(scopes, + deviceCodeResult => CustomDeviceFlowCallback(deviceCodeResult)).ExecuteAsync(); + return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); + } + + public override bool IsSupported(SqlAuthenticationMethod authenticationMethod) => authenticationMethod.Equals(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow); + + private Task CustomDeviceFlowCallback(DeviceCodeResult result) + { + Console.WriteLine(result.Message); + return Task.FromResult(0); + } + } + + public class Program + { + public static void Main() + { + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, new CustomDeviceCodeFlowAzureAuthenticationProvider()); + using (SqlConnection sqlConnection = new SqlConnection("Server=.database.windows.net;Authentication=Active Directory Device Code Flow;Database=;")) + { + sqlConnection.Open(); + Console.WriteLine("Connected successfully!"); + } + } + } +} +// diff --git a/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml b/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml new file mode 100644 index 0000000000..49a2e5616a --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/ActiveDirectoryAuthenticationProvider.xml @@ -0,0 +1,83 @@ + + + + + This class implements and is used for active directory federated authentication mechanisms. + + + + + Initializes the class. + + + + The callback method to be used when performing 'Active Directory Device Code Flow' authentication. + + Initializes the class with the provided device code flow callback method. + + + + The Active Directory authentication parameters passed to authentication providers. + Acquires a security token from the authority. + Represents an asynchronous operation that returns the authentication token. + + + The callback method to be used when performing 'Active Directory Device Code Flow' authentication. + Sets the callback method, overriding the default implementation that processes the result when performing 'Active Directory Device Code Flow' authentication. + + + The parent as an object, in order to be used from shared .NET Standard assemblies. + Sets a reference to the ViewController (if using Xamarin.iOS), Activity (if using Xamarin.Android) IWin32Window or IntPtr (if using .NET Framework). Used for invoking the browser for Active Directory Interactive authentication. + Mandatory to be set only on Android. See https://aka.ms/msal-net-android-activity for further documentation and details. + + + A function to return the current window. + Sets a reference to the current that triggers the browser to be shown. Used to center the browser pop-up onto this window." + + + The callback method to be called by MSAL.NET to delegate the Web user interface with the Secure Token Service (STS). + Sets a callback method which is invoked with a custom Web UI instance that will let the user sign-in with Azure Active Directory, present consent if needed, and get back the authorization code. Applicable when working with Active Directory Interactive authentication. + The "authorizationUri" is crafted to leverage PKCE in order to protect the token from a man in the middle attack. Only MSAL.NET can redeem the code. In the event of cancellation, the implementer should return . + + + The authentication method. + This method is called immediately before the provider is added to authentication provider registry. + Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. + + + The authentication method. + This method is called immediately before the provider is removed from the authentication provider registry. + For example, this method is called when a different provider with the same authentication method overrides this provider in the authentication provider registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry. + + + The authentication method. + Indicates whether the specified authentication method is supported. + + if the specified authentication method is supported; otherwise, . + + + + | +|| +|| +|| +|| + +## Examples + The following example demonstrates providing a custom device flow callback to SqlClient for the Device Code Flow authentication method: + + [!code-csharp[ActiveDirectory_DeviceCodeFlowCallback Example#1](~/../sqlclient/doc/samples/AADAuthenticationCustomDeviceFlowCallback.cs#1)] + + ]]> + + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml index 4bb3bdb696..2ddb84b9a1 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml @@ -29,5 +29,9 @@ The authentication method uses Active Directory Service Principal. Use Active Directory Service Principal to connect to a SQL Database using the client ID and secret of a service principal identity. 5 + + The authentication method uses Active Directory Device Code Flow. Use Active Directory Device Code Flow to connect to a SQL Database from devices and operating systems that do not provide a Web browser, using another device to perform interactive authentication. + 6 + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml index e6f373c300..4f21b9626f 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationProvider.xml @@ -1,49 +1,68 @@ - - - Defines the core behavior of authentication providers and provides a base class for derived classes. - Derived classes must provide a parameterless constructor if they can be instantiated from the app.config file. - - - Called from constructors in derived classes to initialize the class. - - - The authentication method. - Gets an authentication provider by method. - The authentication provider or if not found. - To be added. - - - The authentication method. - The authentication provider. - Sets an authentication provider by method. - - if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). - - To be added. - - - The authentication method. - This method is called immediately before the provider is added to SQL drivers registry. - Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. - - - The authentication method. - This method is called immediately before the provider is removed from the SQL drivers registry. - For example, this method is called when a different provider with the same authentication method overrides this provider in the SQL drivers registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry. - - - The authentication method. - Indicates whether the specified authentication method is supported. - - if the specified authentication method is supported; otherwise, . - To be added. - - - The Active Directory authentication parameters passed by the driver to authentication providers. - Acquires a security token from the authority. - Represents an asynchronous operation that returns the AD authentication token. - To be added. - - + + + Defines the core behavior of authentication providers and provides a base class for derived classes. + + + + + + + + + Called from constructors in derived classes to initialize the class. + + + + The authentication method. + Gets an authentication provider by method. + + The authentication provider or if not found. + + To be added. + + + The authentication method. + The authentication provider. + Sets an authentication provider by method. + + if the operation succeeded; otherwise, (for example, the existing provider disallows overriding). + + To be added. + + + The authentication method. + This method is called immediately before the provider is added to SQL drivers registry. + Avoid performing long-waiting tasks in this method, since it can block other threads from accessing the provider registry. + + + The authentication method. + This method is called immediately before the provider is removed from the SQL drivers registry. + For example, this method is called when a different provider with the same authentication method overrides this provider in the SQL drivers registry. Avoid performing long-waiting task in this method, since it can block other threads from accessing the provider registry. + + + The authentication method. + Indicates whether the specified authentication method is supported. + + if the specified authentication method is supported; otherwise, . + + To be added. + + + The Active Directory authentication parameters passed by the driver to authentication providers. + Acquires a security token from the authority. + Represents an asynchronous operation that returns the AD authentication token. + To be added. + + diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 9077063825..eea16852c3 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -78,6 +78,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.Sql", "Micro EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient", "Microsoft.Data.SqlClient", "{C05F4FFE-6A14-4409-AA0A-10630BE4F1EE}" ProjectSection(SolutionItems) = preProject + ..\doc\snippets\Microsoft.Data.SqlClient\ActiveDirectoryAuthenticationProvider.xml = ..\doc\snippets\Microsoft.Data.SqlClient\ActiveDirectoryAuthenticationProvider.xml ..\doc\snippets\Microsoft.Data.SqlClient\ApplicationIntent.xml = ..\doc\snippets\Microsoft.Data.SqlClient\ApplicationIntent.xml ..\doc\snippets\Microsoft.Data.SqlClient\OnChangeEventHandler.xml = ..\doc\snippets\Microsoft.Data.SqlClient\OnChangeEventHandler.xml ..\doc\snippets\Microsoft.Data.SqlClient\PoolBlockingPeriod.xml = ..\doc\snippets\Microsoft.Data.SqlClient\PoolBlockingPeriod.xml diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetStandard.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetStandard.cs new file mode 100644 index 0000000000..bdb2042b84 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.NetStandard.cs @@ -0,0 +1,16 @@ +// 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. +// ------------------------------------------------------------------------------ +// Changes to this file must follow the http://aka.ms/api-review process. +// ------------------------------------------------------------------------------ + +namespace Microsoft.Data.SqlClient +{ + /// + public sealed partial class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider + { + /// + public void SetParentActivityOrWindowFunc(System.Func parentActivityOrWindowFunc) { } + } +} diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 514a35ad36..c34dcddab7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -30,6 +30,26 @@ public SqlNotificationRequest(string userData, string options, int timeout) { } } namespace Microsoft.Data.SqlClient { + /// + public sealed partial class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider + { + /// + public ActiveDirectoryAuthenticationProvider() { } + /// + public ActiveDirectoryAuthenticationProvider(System.Func deviceCodeFlowCallbackMethod) { } + /// + public override System.Threading.Tasks.Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { throw null; } + /// + public void SetDeviceCodeFlowCallback(System.Func deviceCodeFlowCallbackMethod) { } + /// + public void SetAcquireAuthorizationCodeAsyncCallback(System.Func> acquireAuthorizationCodeAsyncCallback) { } + /// + public override bool IsSupported(SqlAuthenticationMethod authentication) { throw null; } + /// + public override void BeforeLoad(SqlAuthenticationMethod authentication) { } + /// + public override void BeforeUnload(SqlAuthenticationMethod authentication) { } + } /// public enum ApplicationIntent { @@ -69,6 +89,8 @@ public enum SqlAuthenticationMethod ActiveDirectoryPassword = 2, /// ActiveDirectoryServicePrincipal = 5, + /// + ActiveDirectoryDeviceCodeFlow = 6, /// NotSpecified = 0, /// diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index b1934599e6..dad15bc623 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -19,4 +19,10 @@ + + + + + + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 746fe1d9ae..7d6f362461 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -21,6 +21,9 @@ $(DefineConstants);netcoreapp; + + $(DefineConstants);netstandard; + $(DefineConstants);NETCORE3 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 73f005d548..7bd9f2c909 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 @@ -103,10 +103,11 @@ internal static string ConvertToString(object value) const string ActiveDirectoryIntegratedString = "Active Directory Integrated"; const string ActiveDirectoryInteractiveString = "Active Directory Interactive"; const string ActiveDirectoryServicePrincipalString = "Active Directory Service Principal"; + const string ActiveDirectoryDeviceCodeFlowString = "Active Directory Device Code Flow"; internal static bool TryConvertToAuthenticationType(string value, out SqlAuthenticationMethod result) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 6, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 7, "SqlAuthenticationMethod enum has changed, update needed"); bool isSuccess = false; @@ -140,6 +141,12 @@ internal static bool TryConvertToAuthenticationType(string value, out SqlAuthent result = SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; isSuccess = true; } + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryDeviceCodeFlowString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + isSuccess = true; + } else { result = DbConnectionStringDefaults.Authentication; @@ -486,6 +493,7 @@ internal static bool IsValidAuthenticationTypeValue(SqlAuthenticationMethod valu || value == SqlAuthenticationMethod.ActiveDirectoryIntegrated || value == SqlAuthenticationMethod.ActiveDirectoryInteractive || value == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || value == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || value == SqlAuthenticationMethod.NotSpecified; } @@ -505,6 +513,8 @@ internal static string AuthenticationTypeToString(SqlAuthenticationMethod value) return ActiveDirectoryInteractiveString; case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: return ActiveDirectoryServicePrincipalString; + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + return ActiveDirectoryDeviceCodeFlowString; default: return null; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs index 3d6f135df1..eb3e03b1e2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetCoreApp.cs @@ -33,6 +33,7 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); } /// @@ -116,6 +117,8 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe return SqlAuthenticationMethod.ActiveDirectoryInteractive; case ActiveDirectoryServicePrincipal: return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + case ActiveDirectoryDeviceCodeFlow: + return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; default: throw SQL.UnsupportedAuthentication(authentication); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs index e56b491bd7..e3d5fd5e11 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.NetStandard.cs @@ -14,6 +14,7 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryIntegrated, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index 2a22d3c9fa..8d85802486 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -18,6 +18,7 @@ internal partial class SqlAuthenticationProviderManager private const string ActiveDirectoryIntegrated = "active directory integrated"; private const string ActiveDirectoryInteractive = "active directory interactive"; private const string ActiveDirectoryServicePrincipal = "active directory service principal"; + private const string ActiveDirectoryDeviceCodeFlow = "active directory device code flow"; private readonly string _typeName; private readonly IReadOnlyCollection _authenticationsWithAppSpecifiedProvider; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index ea3d80c02d..a139327841 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -147,6 +147,11 @@ public SqlConnection(string connectionString, SqlCredential credential) : this() throw SQL.SettingCredentialWithInteractiveArgument(); } + if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) + { + throw SQL.SettingCredentialWithDeviceFlowArgument(); + } + Credential = credential; } // else @@ -402,6 +407,11 @@ private bool UsesActiveDirectoryInteractive(SqlConnectionString opt) return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive : false; } + private bool UsesActiveDirectoryDeviceCodeFlow(SqlConnectionString opt) + { + return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow : false; + } + private bool UsesAuthentication(SqlConnectionString opt) { return opt != null ? opt.Authentication != SqlAuthenticationMethod.NotSpecified : false; @@ -458,7 +468,7 @@ public override string ConnectionString SqlConnectionString connectionOptions = new SqlConnectionString(value); if (_credential != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive. Since a different error string is used + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive/ActiveDirectoryDeviceCodeFlow. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. if (UsesActiveDirectoryIntegrated(connectionOptions)) @@ -469,6 +479,10 @@ public override string ConnectionString { throw SQL.SettingInteractiveWithCredential(); } + else if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) + { + throw SQL.SettingDeviceFlowWithCredential(); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); } @@ -752,6 +766,10 @@ public SqlCredential Credential { throw SQL.SettingCredentialWithInteractiveInvalid(); } + else if (UsesActiveDirectoryDeviceCodeFlow((SqlConnectionString)ConnectionOptions)) + { + throw SQL.SettingCredentialWithDeviceFlowInvalid(); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions); if (_accessToken != null) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs index 3c9f2116d2..0f0ce90477 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs @@ -171,9 +171,9 @@ override protected DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions connectionTimeout = int.MaxValue; } - if (opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive) + if (opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) { - // interactive mode will always have pool's CreateTimeout = 10 x ConnectTimeout. + // interactive/device code flow mode will always have pool's CreateTimeout = 10 x ConnectTimeout. if (connectionTimeout >= Int32.MaxValue / 10) { connectionTimeout = Int32.MaxValue; @@ -182,7 +182,7 @@ override protected DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions { connectionTimeout *= 10; } - SqlClientEventSource.Log.TraceEvent("Set connection pool CreateTimeout={0} when AD Interactive is in use.", connectionTimeout); + SqlClientEventSource.Log.TraceEvent("Set connection pool CreateTimeout={0} when {1} is in use.", connectionTimeout, opt.Authentication); } poolingOptions = new DbConnectionPoolGroupOptions( opt.IntegratedSecurity, diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index c14bd52ad5..90010dc058 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -462,6 +462,11 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G { throw SQL.InteractiveWithPassword(); } + + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow && (HasUserIdKeyword || HasPasswordKeyword)) + { + throw SQL.DeviceFlowWithUsernamePassword(); + } } // This c-tor is used to create SSE and user instance connection strings when user instance is set to true 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 1999d4365b..3111fee442 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 @@ -1300,6 +1300,7 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, // in Login7, indicating the intent to use Active Directory Authentication for SQL Server. if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired)) @@ -2106,6 +2107,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert((ConnectionOptions.HasUserIdKeyword && ConnectionOptions.HasPasswordKeyword) || _credential != null || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), "Credentials aren't provided for calling MSAL"); Debug.Assert(fedAuthInfo != null, "info should not be null."); @@ -2336,6 +2338,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) } break; case SqlAuthenticationMethod.ActiveDirectoryInteractive: + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { _fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; 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 5c8ec93400..37b42b7512 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 @@ -277,6 +277,10 @@ internal static Exception InteractiveWithPassword() { return ADP.Argument(System.SRHelper.GetString(SR.SQL_InteractiveWithPassword)); } + internal static Exception DeviceFlowWithUsernamePassword() + { + return ADP.Argument(System.SRHelper.GetString(SR.SQL_DeviceFlowWithUsernamePassword)); + } static internal Exception SettingIntegratedWithCredential() { return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_SettingIntegratedWithCredential)); @@ -285,6 +289,10 @@ static internal Exception SettingInteractiveWithCredential() { return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_SettingInteractiveWithCredential)); } + static internal Exception SettingDeviceFlowWithCredential() + { + return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_SettingDeviceFlowWithCredential)); + } static internal Exception SettingCredentialWithIntegratedArgument() { return ADP.Argument(System.SRHelper.GetString(SR.SQL_SettingCredentialWithIntegrated)); @@ -293,6 +301,10 @@ static internal Exception SettingCredentialWithInteractiveArgument() { return ADP.Argument(System.SRHelper.GetString(SR.SQL_SettingCredentialWithInteractive)); } + static internal Exception SettingCredentialWithDeviceFlowArgument() + { + return ADP.Argument(System.SRHelper.GetString(SR.SQL_SettingCredentialWithDeviceFlow)); + } static internal Exception SettingCredentialWithIntegratedInvalid() { return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_SettingCredentialWithIntegrated)); @@ -301,6 +313,10 @@ static internal Exception SettingCredentialWithInteractiveInvalid() { return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_SettingCredentialWithInteractive)); } + static internal Exception SettingCredentialWithDeviceFlowInvalid() + { + return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_SettingCredentialWithDeviceFlow)); + } internal static Exception NullEmptyTransactionName() { return ADP.Argument(System.SRHelper.GetString(SR.SQL_NullEmptyTransactionName)); @@ -447,6 +463,11 @@ internal static Exception ActiveDirectoryInteractiveTimeout() return ADP.TimeoutException(SR.SQL_Timeout_Active_Directory_Interactive_Authentication); } + internal static Exception ActiveDirectoryDeviceFlowTimeout() + { + return ADP.TimeoutException(SR.SQL_Timeout_Active_Directory_DeviceFlow_Authentication); + } + // // SQL.DataCommand diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 1d2dd44c0a..83005d7a03 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -250,6 +250,7 @@ public enum FedAuthLibrary : byte public const byte MSALWORKFLOW_ACTIVEDIRECTORYINTEGRATED = 0x02; public const byte MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE = 0x03; public const byte MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL = 0x01; // Using the Password byte as that is the closest we have + public const byte MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW = 0x03; // Using the Interactive byte as that is the closest we have public enum ActiveDirectoryWorkflow : byte { @@ -257,6 +258,7 @@ public enum ActiveDirectoryWorkflow : byte Integrated = MSALWORKFLOW_ACTIVEDIRECTORYINTEGRATED, Interactive = MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE, ServicePrincipal = MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL, + DeviceCodeFlow = MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW, } // The string used for username in the error message when Authentication = Active Directory Integrated with FedAuth is used, if authentication fails. @@ -1131,6 +1133,9 @@ public enum SqlAuthenticationMethod /// ActiveDirectoryServicePrincipal, + + /// + ActiveDirectoryDeviceCodeFlow, } // This enum indicates the state of TransparentNetworkIPResolution // The first attempt when TNIR is on should be sequential. If the first attempt failes next attempts should be parallel. diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index a1d424120e..4d9c34247b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -394,6 +394,9 @@ internal void Connect( case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: SqlClientEventSource.Log.TraceEvent(" Active Directory Service Principal authentication", "SEC"); break; + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + SqlClientEventSource.Log.TraceEvent(" Active Directory Device Code Flow authentication", "SEC"); + break; case SqlAuthenticationMethod.SqlPassword: SqlClientEventSource.Log.TraceEvent(" SQL Password authentication", "SEC"); break; @@ -7795,6 +7798,9 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL; break; + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW; + break; default: Debug.Assert(false, "Unrecognized Authentication type for fedauth MSAL request"); break; 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 23cbaa4efe..44caedd695 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs @@ -2652,6 +2652,15 @@ internal static string SQL_InteractiveWithPassword { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords.. + /// + internal static string SQL_DeviceFlowWithUsernamePassword { + get { + return ResourceManager.GetString("SQL_DeviceFlowWithUsernamePassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to Internal Error. /// @@ -3057,6 +3066,15 @@ internal static string SQL_SettingCredentialWithInteractive { } } + /// + /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string.. + /// + internal static string SQL_SettingCredentialWithDeviceFlow { + get { + return ResourceManager.GetString("SQL_SettingCredentialWithDeviceFlow", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Integrated', if the Credential property has been set.. /// @@ -3075,6 +3093,15 @@ internal static string SQL_SettingInteractiveWithCredential { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set.. + /// + internal static string SQL_SettingDeviceFlowWithCredential { + get { + return ResourceManager.GetString("SQL_SettingDeviceFlowWithCredential", resourceCulture); + } + } + /// /// Looks up a localized string similar to A severe error occurred on the current command. The results, if any, should be discarded.. /// @@ -3210,6 +3237,15 @@ internal static string SQL_Timeout_Active_Directory_Interactive_Authentication { } } + /// + /// Looks up a localized string similar to Active Directory Device Code Flow authentication timed out. The user took too long to respond to the authentication request.. + /// + internal static string SQL_Timeout_Active_Directory_DeviceFlow_Authentication { + get { + return ResourceManager.GetString("SQL_Timeout_Active_Directory_DeviceFlow_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.. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx index 47d203f9ff..b94391deac 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx @@ -408,6 +408,9 @@ Cannot use 'Authentication=Active Directory Interactive' with 'Password' or 'PWD' connection string keywords. + + Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords. + The instance of SQL Server you attempted to connect to requires encryption but this machine does not support it. @@ -1893,4 +1896,13 @@ Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + Active Directory Device Code Flow authentication timed out. The user took too long to respond to the authentication request. + + + Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string. + + + Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set. + diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 1b044d5373..76d4f44fbb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -35,6 +35,28 @@ public SqlNotificationRequest(string userData, string options, int timeout) { } namespace Microsoft.Data.SqlClient { + /// + public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider + { + /// + public ActiveDirectoryAuthenticationProvider() { } + /// + public ActiveDirectoryAuthenticationProvider(System.Func deviceCodeFlowCallbackMethod) { } + /// + public override System.Threading.Tasks.Task AcquireTokenAsync(SqlAuthenticationParameters parameters) { throw null; } + /// + public void SetDeviceCodeFlowCallback(System.Func deviceCodeFlowCallbackMethod) { } + /// + public void SetAcquireAuthorizationCodeAsyncCallback(System.Func> acquireAuthorizationCodeAsyncCallback) { } + /// + public void SetIWin32WindowFunc(System.Func iWin32WindowFunc) { } + /// + public override bool IsSupported(SqlAuthenticationMethod authentication) { throw null; } + /// + public override void BeforeLoad(SqlAuthenticationMethod authentication) { } + /// + public override void BeforeUnload(SqlAuthenticationMethod authentication) { } + } /// public enum ApplicationIntent { @@ -85,6 +107,8 @@ public enum SqlAuthenticationMethod ActiveDirectoryPassword = 2, /// ActiveDirectoryServicePrincipal = 5, + /// + ActiveDirectoryDeviceCodeFlow = 6, /// NotSpecified = 0, /// diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj index 4d5f63e325..fa07c461b8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -15,4 +15,7 @@ + + + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 6e247cc897..042ba6de5e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -20,6 +20,7 @@ True + $(DefineConstants);netfx; @@ -505,4 +506,4 @@ - + \ No newline at end of file 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 68add3559c..9979e9f6de 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 @@ -521,11 +521,12 @@ internal static ApplicationIntent ConvertToApplicationIntent(string keyword, obj const string ActiveDirectoryIntegratedString = "Active Directory Integrated"; const string ActiveDirectoryInteractiveString = "Active Directory Interactive"; const string ActiveDirectoryServicePrincipalString = "Active Directory Service Principal"; + const string ActiveDirectoryDeviceCodeFlowString = "Active Directory Device Code Flow"; const string SqlCertificateString = "Sql Certificate"; internal static bool TryConvertToAuthenticationType(string value, out SqlAuthenticationMethod result) { - Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 6, "SqlAuthenticationMethod enum has changed, update needed"); + Debug.Assert(Enum.GetNames(typeof(SqlAuthenticationMethod)).Length == 7, "SqlAuthenticationMethod enum has changed, update needed"); bool isSuccess = false; @@ -559,6 +560,12 @@ internal static bool TryConvertToAuthenticationType(string value, out SqlAuthent result = SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; isSuccess = true; } + else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, ActiveDirectoryDeviceCodeFlowString) + || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, CultureInfo.InvariantCulture))) + { + result = SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + isSuccess = true; + } #if ADONET_CERT_AUTH else if (StringComparer.InvariantCultureIgnoreCase.Equals(value, SqlCertificateString) || StringComparer.InvariantCultureIgnoreCase.Equals(value, Convert.ToString(SqlAuthenticationMethod.SqlCertificate, CultureInfo.InvariantCulture))) { @@ -647,6 +654,7 @@ internal static bool IsValidAuthenticationTypeValue(SqlAuthenticationMethod valu || value == SqlAuthenticationMethod.ActiveDirectoryIntegrated || value == SqlAuthenticationMethod.ActiveDirectoryInteractive || value == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || value == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow #if ADONET_CERT_AUTH || value == SqlAuthenticationMethod.SqlCertificate #endif @@ -669,6 +677,8 @@ internal static string AuthenticationTypeToString(SqlAuthenticationMethod value) return ActiveDirectoryInteractiveString; case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: return ActiveDirectoryServicePrincipalString; + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + return ActiveDirectoryDeviceCodeFlowString; #if ADONET_CERT_AUTH case SqlAuthenticationMethod.SqlCertificate: return SqlCertificateString; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs index de50e441b8..144a782acb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlAuthenticationProviderManager.cs @@ -10,8 +10,6 @@ namespace Microsoft.Data.SqlClient { - - /// /// Authentication provider manager. /// @@ -21,6 +19,7 @@ internal class SqlAuthenticationProviderManager private const string ActiveDirectoryIntegrated = "active directory integrated"; private const string ActiveDirectoryInteractive = "active directory interactive"; private const string ActiveDirectoryServicePrincipal = "active directory service principal"; + private const string ActiveDirectoryDeviceCodeFlow = "active directory device code flow"; static SqlAuthenticationProviderManager() { @@ -40,6 +39,7 @@ static SqlAuthenticationProviderManager() Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryPassword, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, activeDirectoryAuthProvider); Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryServicePrincipal, activeDirectoryAuthProvider); + Instance.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, activeDirectoryAuthProvider); } public static readonly SqlAuthenticationProviderManager Instance; @@ -176,6 +176,8 @@ private static SqlAuthenticationMethod AuthenticationEnumFromString(string authe return SqlAuthenticationMethod.ActiveDirectoryInteractive; case ActiveDirectoryServicePrincipal: return SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + case ActiveDirectoryDeviceCodeFlow: + return SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; default: throw SQL.UnsupportedAuthentication(authentication); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index f023b3dacb..a0775464f4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -326,6 +326,11 @@ public SqlConnection(string connectionString, SqlCredential credential) : this() throw SQL.SettingCredentialWithInteractiveArgument(); } + if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) + { + throw SQL.SettingCredentialWithDeviceFlowArgument(); + } + Credential = credential; } // else @@ -515,6 +520,11 @@ private bool UsesActiveDirectoryInteractive(SqlConnectionString opt) return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive : false; } + private bool UsesActiveDirectoryDeviceCodeFlow(SqlConnectionString opt) + { + return opt != null ? opt.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow : false; + } + private bool UsesAuthentication(SqlConnectionString opt) { return opt != null ? opt.Authentication != SqlAuthenticationMethod.NotSpecified : false; @@ -658,7 +668,7 @@ override public string ConnectionString SqlConnectionString connectionOptions = new SqlConnectionString(value); if (_credential != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive. Since a different error string is used + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive/ActiveDirectoryDeviceCodeFlow. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. if (UsesActiveDirectoryIntegrated(connectionOptions)) @@ -669,6 +679,10 @@ override public string ConnectionString { throw SQL.SettingInteractiveWithCredential(); } + else if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions)) + { + throw SQL.SettingDeviceFlowWithCredential(); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions); } @@ -983,7 +997,7 @@ public SqlCredential Credential // check if the usage of credential has any conflict with the keys used in connection string if (value != null) { - // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive. Since a different error string is used + // Check for Credential being used with Authentication=ActiveDirectoryIntegrated/ActiveDirectoryInteractive/ActiveDirectoryDeviceCodeFlow. Since a different error string is used // for this case in ConnectionString setter vs in Credential setter, check for this error case before calling // CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential, which is common to both setters. if (UsesActiveDirectoryIntegrated((SqlConnectionString)ConnectionOptions)) @@ -994,6 +1008,10 @@ public SqlCredential Credential { throw SQL.SettingCredentialWithInteractiveInvalid(); } + else if (UsesActiveDirectoryDeviceCodeFlow((SqlConnectionString)ConnectionOptions)) + { + throw SQL.SettingCredentialWithDeviceFlowInvalid(); + } CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions); if (_accessToken != null) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs index 972e97ae91..d696512b51 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionString.cs @@ -551,6 +551,11 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G throw SQL.InteractiveWithPassword(); } + if (Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow && (HasUserIdKeyword || HasPasswordKeyword)) + { + throw SQL.DeviceFlowWithUsernamePassword(); + } + #if ADONET_CERT_AUTH if (!DbConnectionStringBuilderUtil.IsValidCertificateValue(_certificate)) { 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 3d033f5a60..49f947acf2 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 @@ -1567,12 +1567,13 @@ private void Login(ServerInfo server, TimeoutTimer timeout, string newPassword, _sessionRecoveryRequested = true; } - // If the workflow being used is Active Directory Password/Integrated/Interactive/Service Principal and server's prelogin response + // If the workflow being used is Active Directory Password/Integrated/Interactive/Service Principal/Device Code Flow and server's prelogin response // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature Extension // in Login7, indicating the intent to use Active Directory Authentication for SQL Server. if (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow // Since AD Integrated may be acting like Windows integrated, additionally check _fedAuthRequired || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired)) { @@ -1964,7 +1965,8 @@ private bool ShouldDisableTnir(SqlConnectionString connectionOptions) connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive || - connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal || + connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; // Check if the user had explicitly specified the TNIR option in the connection string or the connection string builder. // If the user has specified the option in the connection string explicitly, then we shouldn't disable TNIR. @@ -2550,6 +2552,7 @@ internal void OnFedAuthInfo(SqlFedAuthInfo fedAuthInfo) Debug.Assert((ConnectionOptions.HasUserIdKeyword && ConnectionOptions.HasPasswordKeyword) || _credential != null || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow || (ConnectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated && _fedAuthRequired), "Credentials aren't provided for calling MSAL"); Debug.Assert(fedAuthInfo != null, "info should not be null."); @@ -2770,6 +2773,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo) } break; case SqlAuthenticationMethod.ActiveDirectoryInteractive: + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying) { fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken; 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 6cb5b7ed8c..6318a1491f 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 @@ -325,12 +325,14 @@ static internal Exception IntegratedWithUserIDAndPassword() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_IntegratedWithUserIDAndPassword)); } - static internal Exception InteractiveWithPassword() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_InteractiveWithPassword)); } - + static internal Exception DeviceFlowWithUsernamePassword() + { + return ADP.Argument(StringsHelper.GetString(Strings.SQL_DeviceFlowWithUsernamePassword)); + } static internal Exception SettingIntegratedWithCredential() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingIntegratedWithCredential)); @@ -339,6 +341,10 @@ static internal Exception SettingInteractiveWithCredential() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingInteractiveWithCredential)); } + static internal Exception SettingDeviceFlowWithCredential() + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingDeviceFlowWithCredential)); + } static internal Exception SettingCredentialWithIntegratedArgument() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_SettingCredentialWithIntegrated)); @@ -347,6 +353,10 @@ static internal Exception SettingCredentialWithInteractiveArgument() { return ADP.Argument(StringsHelper.GetString(Strings.SQL_SettingCredentialWithInteractive)); } + static internal Exception SettingCredentialWithDeviceFlowArgument() + { + return ADP.Argument(StringsHelper.GetString(Strings.SQL_SettingCredentialWithDeviceFlow)); + } static internal Exception SettingCredentialWithIntegratedInvalid() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingCredentialWithIntegrated)); @@ -355,6 +365,10 @@ static internal Exception SettingCredentialWithInteractiveInvalid() { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingCredentialWithInteractive)); } + static internal Exception SettingCredentialWithDeviceFlowInvalid() + { + return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_SettingCredentialWithDeviceFlow)); + } static internal Exception InvalidSQLServerVersionUnknown() { return ADP.DataAdapter(StringsHelper.GetString(Strings.SQL_InvalidSQLServerVersionUnknown)); @@ -478,6 +492,11 @@ static internal Exception ActiveDirectoryInteractiveTimeout() return ADP.TimeoutException(Strings.SQL_Timeout_Active_Directory_Interactive_Authentication); } + static internal Exception ActiveDirectoryDeviceFlowTimeout() + { + return ADP.TimeoutException(Strings.SQL_Timeout_Active_Directory_DeviceFlow_Authentication); + } + // // SQL.DataCommand // diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index cf8df1524b..c6ad7fea14 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -242,6 +242,7 @@ public enum FedAuthLibrary : byte public const byte MSALWORKFLOW_ACTIVEDIRECTORYINTEGRATED = 0x02; public const byte MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE = 0x03; public const byte MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL = 0x01; // Using the Password byte as that is the closest we have + public const byte MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW = 0x03; // Using the Interactive byte as that is the closest we have public enum ActiveDirectoryWorkflow : byte { @@ -249,6 +250,7 @@ public enum ActiveDirectoryWorkflow : byte Integrated = MSALWORKFLOW_ACTIVEDIRECTORYINTEGRATED, Interactive = MSALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE, ServicePrincipal = MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL, + DeviceCodeFlow = MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW, } // The string used for username in the error message when Authentication = Active Directory Integrated with FedAuth is used, if authentication fails. @@ -1093,6 +1095,9 @@ public enum SqlAuthenticationMethod /// ActiveDirectoryServicePrincipal, + + /// + ActiveDirectoryDeviceCodeFlow, #if ADONET_CERT_AUTH SqlCertificate #endif diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 795a0ff841..03404784e0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -545,6 +545,9 @@ internal void Connect(ServerInfo serverInfo, case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: SqlClientEventSource.Log.TraceEvent(" Active Directory Service Principal authentication", "SEC"); break; + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + SqlClientEventSource.Log.TraceEvent(" Active Directory Device Code Flow authentication", "SEC"); + break; case SqlAuthenticationMethod.SqlPassword: SqlClientEventSource.Log.TraceEvent(" SQL Password authentication", "SEC"); break; @@ -8557,6 +8560,9 @@ internal int WriteFedAuthFeatureRequest(FederatedAuthenticationFeatureExtensionD case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal: workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL; break; + case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow: + workflow = TdsEnums.MSALWORKFLOW_ACTIVEDIRECTORYDEVICECODEFLOW; + break; default: Debug.Assert(false, "Unrecognized Authentication type for fedauth MSAL request"); break; 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 b6b1a532df..0501c22c3e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -9441,6 +9441,15 @@ internal static string SQL_InteractiveWithPassword { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords.. + /// + internal static string SQL_DeviceFlowWithUsernamePassword { + get { + return ResourceManager.GetString("SQL_DeviceFlowWithUsernamePassword", resourceCulture); + } + } + /// /// Looks up a localized string similar to Internal Error. /// @@ -9918,6 +9927,15 @@ internal static string SQL_SettingCredentialWithInteractive { } } + /// + /// Looks up a localized string similar to Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string.. + /// + internal static string SQL_SettingCredentialWithDeviceFlow { + get { + return ResourceManager.GetString("SQL_SettingCredentialWithDeviceFlow", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Integrated', if the Credential property has been set.. /// @@ -9936,6 +9954,15 @@ internal static string SQL_SettingInteractiveWithCredential { } } + /// + /// Looks up a localized string similar to Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set.. + /// + internal static string SQL_SettingDeviceFlowWithCredential { + get { + return ResourceManager.GetString("SQL_SettingDeviceFlowWithCredential", resourceCulture); + } + } + /// /// Looks up a localized string similar to A severe error occurred on the current command. The results, if any, should be discarded.. /// @@ -10152,6 +10179,15 @@ internal static string SQL_Timeout_Active_Directory_Interactive_Authentication { } } + /// + /// Looks up a localized string similar to Active Directory Device Code Flow authentication timed out. The user took too long to respond to the authentication request.. + /// + internal static string SQL_Timeout_Active_Directory_DeviceFlow_Authentication { + get { + return ResourceManager.GetString("SQL_Timeout_Active_Directory_DeviceFlow_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.. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index 1a90bdf991..c74e4526ef 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -2499,6 +2499,9 @@ Cannot use 'Authentication=Active Directory Interactive' with 'Password' or 'PWD' connection string keywords. + + Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords. + Cannot use 'Authentication=Active Directory Integrated', if the Credential property has been set. @@ -4563,4 +4566,13 @@ Server Process Id (SPID) of the active connection. + + Active Directory Device Code Flow authentication timed out. The user took too long to respond to the authentication request. + + + Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string. + + + Cannot use 'Authentication=Active Directory Device Code Flow', if the Credential property has been set. + 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 7b5541fa6b..fe3317f17b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/ActiveDirectoryAuthenticationProvider.cs @@ -3,28 +3,76 @@ // 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; +using Microsoft.Identity.Client.Extensibility; namespace Microsoft.Data.SqlClient { - - /// - /// Default auth provider for AD Integrated. - /// - internal class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider + /// + public sealed class ActiveDirectoryAuthenticationProvider : SqlAuthenticationProvider { private static readonly string s_defaultScopeSuffix = "/.default"; private readonly string _type = typeof(ActiveDirectoryAuthenticationProvider).Name; private readonly SqlClientLogger _logger = new SqlClientLogger(); + private Func _deviceCodeFlowCallback; + private ICustomWebUi _customWebUI = null; + + /// + public ActiveDirectoryAuthenticationProvider() => new ActiveDirectoryAuthenticationProvider(DefaultDeviceFlowCallback); + + /// + public ActiveDirectoryAuthenticationProvider(Func deviceCodeFlowCallbackMethod) + { + SetDeviceCodeFlowCallback(deviceCodeFlowCallbackMethod); + } + + /// + public void SetDeviceCodeFlowCallback(Func deviceCodeFlowCallbackMethod) => _deviceCodeFlowCallback = deviceCodeFlowCallbackMethod; + + /// + public void SetAcquireAuthorizationCodeAsyncCallback(Func> acquireAuthorizationCodeAsyncCallback) => _customWebUI = new CustomWebUi(acquireAuthorizationCodeAsyncCallback); + + /// + public override bool IsSupported(SqlAuthenticationMethod authentication) + { + return authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated + || authentication == SqlAuthenticationMethod.ActiveDirectoryPassword + || authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive + || authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal + || authentication == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow; + } + + /// + public override void BeforeLoad(SqlAuthenticationMethod authentication) + { + _logger.LogInfo(_type, "BeforeLoad", $"being loaded into SqlAuthProviders for {authentication}."); + } - /// - /// Get token. - /// + /// + public override void BeforeUnload(SqlAuthenticationMethod authentication) + { + _logger.LogInfo(_type, "BeforeUnload", $"being unloaded from SqlAuthProviders for {authentication}."); + } + +#if netstandard + private Func parentActivityOrWindowFunc = null; + + /// + public void SetParentActivityOrWindowFunc(Func parentActivityOrWindowFunc) => this.parentActivityOrWindowFunc = parentActivityOrWindowFunc; +#endif + +#if netfx + private Func _iWin32WindowFunc = null; + + /// + public void SetIWin32WindowFunc(Func iWin32WindowFunc) => this._iWin32WindowFunc = iWin32WindowFunc; +#endif + + /// public override Task AcquireTokenAsync(SqlAuthenticationParameters parameters) => Task.Run(async () => { AuthenticationResult result; @@ -44,23 +92,58 @@ public override Task AcquireTokenAsync(SqlAuthentication return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); } - IPublicClientApplication app = PublicClientApplicationBuilder.Create(ActiveDirectoryAuthentication.AdoClientId) + /* + * 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 = "https://login.microsoftonline.com/common/oauth2/nativeclient"; + +#if netcoreapp + if(parameters.AuthenticationMethod != SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) + { + redirectURI = "http://localhost"; + } +#endif + IPublicClientApplication app; + +#if netstandard + if (parentActivityOrWindowFunc != null) + { + app = PublicClientApplicationBuilder.Create(ActiveDirectoryAuthentication.AdoClientId) .WithAuthority(parameters.Authority) .WithClientName(Common.DbConnectionStringDefaults.ApplicationName) .WithClientVersion(Common.ADP.GetAssemblyVersion().ToString()) -#if netcoreapp - .WithRedirectUri("http://localhost") -#else - /* - * 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 - */ - .WithRedirectUri("https://login.microsoftonline.com/oauth2/nativeclient") + .WithRedirectUri(redirectURI) + .WithParentActivityOrWindow(parentActivityOrWindowFunc) + .Build(); + } #endif +#if netfx + if (_iWin32WindowFunc != null) + { + app = PublicClientApplicationBuilder.Create(ActiveDirectoryAuthentication.AdoClientId) + .WithAuthority(parameters.Authority) + .WithClientName(Common.DbConnectionStringDefaults.ApplicationName) + .WithClientVersion(Common.ADP.GetAssemblyVersion().ToString()) + .WithRedirectUri(redirectURI) + .WithParentActivityOrWindow(_iWin32WindowFunc) .Build(); + } +#endif +#if !netcoreapp + else +#endif + { + app = PublicClientApplicationBuilder.Create(ActiveDirectoryAuthentication.AdoClientId) + .WithAuthority(parameters.Authority) + .WithClientName(Common.DbConnectionStringDefaults.ApplicationName) + .WithClientVersion(Common.ADP.GetAssemblyVersion().ToString()) + .WithRedirectUri(redirectURI) + .Build(); + } if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryIntegrated) { @@ -88,9 +171,10 @@ public override Task AcquireTokenAsync(SqlAuthentication .WithCorrelationId(parameters.ConnectionId) .ExecuteAsync().Result; } - else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) + else if (parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive || + parameters.AuthenticationMethod == SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow) { - var accounts = await app.GetAccountsAsync(); + System.Collections.Generic.IEnumerable accounts = await app.GetAccountsAsync(); IAccount account; if (!string.IsNullOrEmpty(parameters.UserId)) { @@ -109,12 +193,12 @@ public override Task AcquireTokenAsync(SqlAuthentication } catch (MsalUiRequiredException) { - result = await AcquireTokenInteractive(app, scopes, parameters.ConnectionId, parameters.UserId); + result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod); } } else { - result = await AcquireTokenInteractive(app, scopes, parameters.ConnectionId, parameters.UserId); + result = await AcquireTokenInteractiveDeviceFlowAsync(app, scopes, parameters.ConnectionId, parameters.UserId, parameters.AuthenticationMethod); } } else @@ -125,7 +209,9 @@ public override Task AcquireTokenAsync(SqlAuthentication return new SqlAuthenticationToken(result.AccessToken, result.ExpiresOn); }); - private async Task AcquireTokenInteractive(IPublicClientApplication app, string[] scopes, Guid connectionId, string userId) + + private async Task AcquireTokenInteractiveDeviceFlowAsync(IPublicClientApplication app, string[] scopes, Guid connectionId, string userId, + SqlAuthenticationMethod authenticationMethod) { CancellationTokenSource cts = new CancellationTokenSource(); #if netcoreapp @@ -142,55 +228,80 @@ private async Task AcquireTokenInteractive(IPublicClientAp #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); + if (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) + { + if (_customWebUI != null) + { + return await app.AcquireTokenInteractive(scopes) + .WithCorrelationId(connectionId) + .WithCustomWebUi(_customWebUI) + .WithLoginHint(userId) + .ExecuteAsync(cts.Token); + } + 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(cts.Token); + } + } + else + { + AuthenticationResult result = await app.AcquireTokenWithDeviceCode(scopes, + deviceCodeResult => _deviceCodeFlowCallback(deviceCodeResult)).ExecuteAsync(); + return result; + } } catch (OperationCanceledException) { - throw SQL.ActiveDirectoryInteractiveTimeout(); + throw (authenticationMethod == SqlAuthenticationMethod.ActiveDirectoryInteractive) ? + SQL.ActiveDirectoryInteractiveTimeout() : + SQL.ActiveDirectoryDeviceFlowTimeout(); } } - /// - /// Checks support for authentication type in lower case. - /// Interactive authentication added. - /// - public override bool IsSupported(SqlAuthenticationMethod authentication) + private Task DefaultDeviceFlowCallback(DeviceCodeResult result) { - return authentication == SqlAuthenticationMethod.ActiveDirectoryIntegrated - || authentication == SqlAuthenticationMethod.ActiveDirectoryPassword - || authentication == SqlAuthenticationMethod.ActiveDirectoryInteractive - || authentication == SqlAuthenticationMethod.ActiveDirectoryServicePrincipal; + // This will print the message on the console which tells the user where to go sign-in using + // a separate browser and the code to enter once they sign in. + // The AcquireTokenWithDeviceCode() method will poll the server after firing this + // device code callback to look for the successful login of the user via that browser. + // This background polling (whose interval and timeout data is also provided as fields in the + // deviceCodeCallback class) will occur until: + // * The user has successfully logged in via browser and entered the proper code + // * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached + // * The developing application calls the Cancel() method on a CancellationToken sent into the method. + // If this occurs, an OperationCanceledException will be thrown (see catch below for more details). + Console.WriteLine(result.Message); + return Task.FromResult(0); } - public override void BeforeLoad(SqlAuthenticationMethod authentication) + private class CustomWebUi : ICustomWebUi { - _logger.LogInfo(_type, "BeforeLoad", $"being loaded into SqlAuthProviders for {authentication}."); - } + private readonly Func> _acquireAuthorizationCodeAsyncCallback; - public override void BeforeUnload(SqlAuthenticationMethod authentication) - { - _logger.LogInfo(_type, "BeforeUnload", $"being unloaded from SqlAuthProviders for {authentication}."); + internal CustomWebUi(Func> acquireAuthorizationCodeAsyncCallback) => _acquireAuthorizationCodeAsyncCallback = acquireAuthorizationCodeAsyncCallback; + + public Task AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken) + => _acquireAuthorizationCodeAsyncCallback.Invoke(authorizationUri, redirectUri, cancellationToken); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAccessTokenTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs similarity index 72% rename from src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAccessTokenTest.cs rename to src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs index ffb21e767b..0e695cdd4e 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAccessTokenTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/AADAuthenticationTests.cs @@ -4,11 +4,13 @@ using System; using System.Security; +using System.Threading.Tasks; +using Microsoft.Identity.Client; using Xunit; namespace Microsoft.Data.SqlClient.Tests { - public class AADAccessTokenTest + public class AADAuthenticationTests { private SqlConnectionStringBuilder _builder; private SqlCredential _credential = null; @@ -38,6 +40,7 @@ public void InvalidCombinationOfAccessToken(string description, object[] Params) InvalidCombinationCheck(_credential); } + private void InvalidCombinationCheck(SqlCredential credential) { using (SqlConnection connection = new SqlConnection(_builder.ConnectionString, credential)) @@ -45,5 +48,19 @@ private void InvalidCombinationCheck(SqlCredential credential) Assert.Throws(() => connection.AccessToken = "SampleAccessToken"); } } + + [Fact] + public void CustomActiveDirectoryProviderTest() + { + SqlAuthenticationProvider authProvider = new ActiveDirectoryAuthenticationProvider(CustomDeviceFlowCallback); + SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow, authProvider); + Assert.Equal(authProvider, SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow)); + } + + private Task CustomDeviceFlowCallback(DeviceCodeResult result) + { + Console.WriteLine(result.Message); + return Task.FromResult(0); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index c0cdf6fd85..ed7e701212 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -31,7 +31,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs index c4014b4d19..68919c39eb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/AADConnectionTest.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; +using System.Security; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -307,7 +308,7 @@ public static void NoCredentialsActiveDirectoryServicePrincipal() // test Passes with correct connection string. string[] removeKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; string connStr = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, removeKeys) + - $"Authentication=Active Directory Service Principal; User ID={DataTestUtility.AADServicePrincipalId}; Password={DataTestUtility.AADServicePrincipalSecret};"; + $"Authentication=Active Directory Service Principal; User ID={DataTestUtility.AADServicePrincipalId}; PWD={DataTestUtility.AADServicePrincipalSecret};"; ConnectAndDisconnect(connStr); // connection fails with expected error message. @@ -320,7 +321,41 @@ public static void NoCredentialsActiveDirectoryServicePrincipal() Assert.Contains(expectedMessage, e.Message); } - [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsIntegratedSecuritySetup), nameof(DataTestUtility.AreConnStringsSetup)) ] + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryDeviceCodeFlowWithUserIdMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithUID = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=Active Directory Device Code Flow; UID=someuser;"; + ArgumentException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithUID)); + + string expectedMessage = "Cannot use 'Authentication=Active Directory Device Code Flow' with 'User ID', 'UID', 'Password' or 'PWD' connection string keywords."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact(nameof(IsAADConnStringsSetup))] + public static void ActiveDirectoryDeviceCodeFlowWithCredentialsMustFail() + { + // connection fails with expected error message. + string[] credKeys = { "Authentication", "User ID", "Password", "UID", "PWD" }; + string connStrWithNoCred = DataTestUtility.RemoveKeysInConnStr(DataTestUtility.AADPasswordConnectionString, credKeys) + + "Authentication=Active Directory Device Code Flow;"; + + SecureString str = new SecureString(); + foreach (char c in "hello") + { + str.AppendChar(c); + } + str.MakeReadOnly(); + SqlCredential credential = new SqlCredential("someuser", str); + InvalidOperationException e = Assert.Throws(() => ConnectAndDisconnect(connStrWithNoCred, credential)); + + string expectedMessage = "Cannot set the Credential property if 'Authentication=Active Directory Device Code Flow' has been specified in the connection string."; + Assert.Contains(expectedMessage, e.Message); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsIntegratedSecuritySetup), nameof(DataTestUtility.AreConnStringsSetup))] public static void ADInteractiveUsingSSPI() { // test Passes with correct connection string.