Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce support for AAD Device Code Flow authentication #597

Merged
merged 17 commits into from Jul 27, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
56 changes: 56 additions & 0 deletions doc/samples/CustomDeviceCodeFlowAzureAuthenticationProvider.cs
@@ -0,0 +1,56 @@
//<Snippet1>
using System;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.Data.SqlClient;

namespace CustomAuthenticationProviderExamples
{
/// <summary>
/// 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.
/// </summary>
public class CustomDeviceCodeFlowAzureAuthenticationProvider : SqlAuthenticationProvider
{
public override async Task<SqlAuthenticationToken> AcquireTokenAsync(SqlAuthenticationParameters parameters)
{
string clientId = "my-client-id";
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
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();
cheenamalhotra marked this conversation as resolved.
Show resolved Hide resolved
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=<myserver>.database.windows.net;Authentication=Active Directory Device Code Flow;Database=<db>;"))
{
sqlConnection.Open();
Console.WriteLine("Connected successfully!");
}
}
}
}
//</Snippet1>
Expand Up @@ -29,5 +29,9 @@
<summary>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.</summary>
<value>5</value>
</ActiveDirectoryServicePrincipal>
<ActiveDirectoryDeviceCodeFlow>
<summary>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.</summary>
<value>6</value>
</ActiveDirectoryDeviceCodeFlow>
</members>
</docs>
Expand Up @@ -69,6 +69,8 @@ public enum SqlAuthenticationMethod
ActiveDirectoryPassword = 2,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml' path='docs/members[@name="SqlAuthenticationMethod"]/ActiveDirectoryServicePrincipal/*'/>
ActiveDirectoryServicePrincipal = 5,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml' path='docs/members[@name="SqlAuthenticationMethod"]/ActiveDirectoryDeviceCodeFlow/*'/>
ActiveDirectoryDeviceCodeFlow = 6,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml' path='docs/members[@name="SqlAuthenticationMethod"]/NotSpecified/*'/>
NotSpecified = 0,
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlAuthenticationMethod.xml' path='docs/members[@name="SqlAuthenticationMethod"]/SqlPassword/*'/>
Expand Down
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand Down
Expand Up @@ -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);
}

/// <summary>
Expand Down Expand Up @@ -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);
}
Expand Down
Expand Up @@ -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);
}
}
}
Expand Up @@ -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<SqlAuthenticationMethod> _authenticationsWithAppSpecifiedProvider;
Expand Down
Expand Up @@ -146,6 +146,11 @@ public SqlConnection(string connectionString, SqlCredential credential) : this()
throw SQL.SettingCredentialWithInteractiveArgument();
}

if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions))
{
throw SQL.SettingCredentialWithDeviceFlowArgument();
}

Credential = credential;
}
// else
Expand Down Expand Up @@ -401,6 +406,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;
Expand Down Expand Up @@ -457,7 +467,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))
Expand All @@ -468,6 +478,10 @@ public override string ConnectionString
{
throw SQL.SettingInteractiveWithCredential();
}
else if (UsesActiveDirectoryDeviceCodeFlow(connectionOptions))
{
throw SQL.SettingDeviceFlowWithCredential();
}

CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential(connectionOptions);
}
Expand Down Expand Up @@ -698,6 +712,10 @@ public SqlCredential Credential
{
throw SQL.SettingCredentialWithInteractiveInvalid();
}
else if (UsesActiveDirectoryDeviceCodeFlow((SqlConnectionString)ConnectionOptions))
{
throw SQL.SettingCredentialWithDeviceFlowInvalid();
}

CheckAndThrowOnInvalidCombinationOfConnectionStringAndSqlCredential((SqlConnectionString)ConnectionOptions);
if (_accessToken != null)
Expand Down
Expand Up @@ -173,9 +173,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;
Expand All @@ -184,7 +184,7 @@ override protected DbConnectionPoolGroupOptions CreateConnectionPoolGroupOptions
{
connectionTimeout *= 10;
}
SqlClientEventSource.Log.TraceEvent("<sc.SqlConnectionFactory.CreateConnectionPoolGroupOptions>Set connection pool CreateTimeout={0} when AD Interactive is in use.", connectionTimeout);
SqlClientEventSource.Log.TraceEvent("<sc.SqlConnectionFactory.CreateConnectionPoolGroupOptions>Set connection pool CreateTimeout={0} when {1} is in use.", connectionTimeout, opt.Authentication);
}
poolingOptions = new DbConnectionPoolGroupOptions(
opt.IntegratedSecurity,
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -1217,6 +1217,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))
Expand Down Expand Up @@ -2020,6 +2021,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.");
Expand Down Expand Up @@ -2251,6 +2253,7 @@ internal SqlFedAuthToken GetFedAuthToken(SqlFedAuthInfo fedAuthInfo)
}
break;
case SqlAuthenticationMethod.ActiveDirectoryInteractive:
case SqlAuthenticationMethod.ActiveDirectoryDeviceCodeFlow:
if (_activeDirectoryAuthTimeoutRetryHelper.State == ActiveDirectoryAuthenticationTimeoutRetryState.Retrying)
{
fedAuthToken = _activeDirectoryAuthTimeoutRetryHelper.CachedToken;
Expand Down
Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -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
Expand Down