【问题标题】:Connect to Multiple Azure DBs with AAD/MFA Credentials使用 AAD/MFA 凭据连接到多个 Azure DB
【发布时间】:2020-10-30 02:58:27
【问题描述】:

我正在尝试编写一个连接到多个 Azure SQL 数据库的 netcore 控制台应用程序,并针对它们执行一些脚本。我们公司需要具有 MFA 登录的 Azure AD 数据库。

我已经设法让它成功登录,使用信息here

设置

static void Main(string[] args)
{
    var provider = new ActiveDirectoryAuthProvider();

    SqlAuthenticationProvider.SetProvider(
        SqlAuthenticationMethod.ActiveDirectoryIntegrated,
        //SC.SqlAuthenticationMethod.ActiveDirectoryInteractive,
        //SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated,  // Alternatives.
        //SC.SqlAuthenticationMethod.ActiveDirectoryPassword,
        provider);
}

public class ActiveDirectoryAuthProvider : SqlAuthenticationProvider
{
    // Program._ more static values that you set!
    private readonly string _clientId = "MyClientID";

    public override async TT.Task<SC.SqlAuthenticationToken>
        AcquireTokenAsync(SC.SqlAuthenticationParameters parameters)
    {
        AD.AuthenticationContext authContext =
            new AD.AuthenticationContext(parameters.Authority);
        authContext.CorrelationId = parameters.ConnectionId;
        AD.AuthenticationResult result;

        switch (parameters.AuthenticationMethod)
        {
             case SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated:
                Console.WriteLine("In method 'AcquireTokenAsync', case_1 == '.ActiveDirectoryIntegrated'.");
                Console.WriteLine($"Resource: {parameters.Resource}");

                result = await authContext.AcquireTokenAsync(
                    parameters.Resource,
                    _clientId,
                    new AD.UserCredential(GlobalSettings.CredentialsSettings.Username));
                break;

            default: throw new InvalidOperationException();
        }           

        return new SC.SqlAuthenticationToken(result.AccessToken, result.ExpiresOn);
    }

    public override bool IsSupported(SC.SqlAuthenticationMethod authenticationMethod)
    {
        return authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated
            || authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryInteractive;
    }
}

连接

private SqlConnection GetConnection()
{
    var builder = new SqlConnectionStringBuilder();
    builder.DataSource = "MyServer";            
    builder.Encrypt = true;
    builder.TrustServerCertificate = true;
    builder.PersistSecurityInfo = true;
    builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;
    builder.InitialCatalog = "MyDatabase";

    var conn = new SqlConnection(builder.ToString());
    conn.Open();

    return conn;        
}

这行得通,我可以随心所欲地运行查询。但是,每当应用程序连接到一个新数据库(在同一地址)时,它都会打开一个浏览器窗口以访问 login.microsoftonline.com,要求我选择我的帐户/登录。

有没有办法要求所有数据库只进行一次浏览器身份验证?它们都在同一个 Azure SQL 实例上。

【问题讨论】:

  • @jdweng Integrated Security 似乎根本不起作用。每当我尝试时,我都会收到此错误:Windows logins are not supported in this version of SQL Server.
  • @jdweng 这是 Azure SQL 数据库,而不是直接的 SQL。它还被设置为需要带有 MFA 的 AD。
  • @jdweng 我不确定你明白我的意思:我没有在 Azure 中使用 SQL 数据库(例如在 VM 中)。我的意思是我正在使用 Azure SQL 数据库 (link) - 我必须明确配置 AD MFA 才能使用 Azure Data Studio。
  • @jdweng WAT? azure和sql之间没有数据源连接。 Azure SQL 数据库是 MS 的特定产品。这是一个主机 sql 数据库,在 Azure 中,由 MS 维护。它始终是最新的 SQL 版本。我们在这里陷入了困境:我已经澄清了产品并指出(根据文档)windows auth(即集成身份验证)不起作用
  • @jdweng 好的,我们显然已经完成了。它不是 MS Access,从 2016 年开始对它的回答也无济于事。这是一个实际的(托管的)SQL 服务器数据库,在 current 发布版本(MS 维护它)。根据链接的文档,集成安全不起作用

标签: c# sql-server .net-core azure-active-directory azure-sql-database


【解决方案1】:

所以,代码中有一点 PEBKAC。尽管它使用了builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;,但该类实际上是在尝试使用ActiveDirectoryIntegrated。所以我的AD课从未真正被击中。此外,在示例代码中它实际上也不会起作用,因为 ActiveDirectoryIntegrated 的 case 语句存在 - 我已经在我的本地副本中删除了它。

我实际上需要使用正确的ActiveDirectoryInteractive 代码来连接它。一旦我这样做了,它就能够对系统进行一次身份验证。这使得所有数据库连接都可以正常工作,而无需额外的浏览器检查。

设置

static void Main(string[] args)
{
    var provider = new ActiveDirectoryAuthProvider();

    SqlAuthenticationProvider.SetProvider(
        SqlAuthenticationMethod.ActiveDirectoryInteractive,
        //SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated,  // Alternatives.
        //SC.SqlAuthenticationMethod.ActiveDirectoryPassword,
        provider);
}

ActiveDirectoryAuthProvider

public class ActiveDirectoryAuthProvider : SqlAuthenticationProvider
{
    private readonly string _clientId = "MyClientID";

    private Uri _redirectURL { get; set; } = new Uri("http://localhost:8089");

    private AD.AuthenticationContext AuthContext { get; set; }

    private TokenCache Cache { get; set; }

    public ActiveDirectoryAuthProvider()
    {
        Cache = new TokenCache();
    }

    public override async TT.Task<SC.SqlAuthenticationToken> AcquireTokenAsync(SC.SqlAuthenticationParameters parameters)
    {
        var authContext = AuthContext ?? new AD.AuthenticationContext(parameters.Authority, Cache);
        authContext.CorrelationId = parameters.ConnectionId;
        AD.AuthenticationResult result;

        try
        {
            result = await authContext.AcquireTokenSilentAsync(
                parameters.Resource,
                _clientId);     
        }
        catch (AdalSilentTokenAcquisitionException)
        {
            result = await authContext.AcquireTokenAsync(
                parameters.Resource,
                _clientId,
                _redirectURL, 
                new AD.PlatformParameters(PromptBehavior.Auto, new CustomWebUi()), 
                new UserIdentifier(parameters.UserId, UserIdentifierType.RequiredDisplayableId));
        }         

        var token = new SC.SqlAuthenticationToken(result.AccessToken, result.ExpiresOn);

        return token;
    }

    public override bool IsSupported(SC.SqlAuthenticationMethod authenticationMethod)
    {
        return authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryInteractive;
    }
}

这里有一些不同之处:

  1. 我添加了一个内存令牌缓存
  2. 我已将 AuthContext 移动到类的属性中,以便在运行之间将其保留在内存中
  3. 我设置了 _redirectURL 属性 = http://localhost:8089
  4. 在还原之前,我添加了对令牌的静默检查

最后,我创建了自己的 ICustomWebUi 实现,用于处理加载浏览器登录和响应:

CustomWebUi

internal class CustomWebUi : ICustomWebUi
{
    public async Task<Uri> AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri)
    {
        using (var listener = new SingleMessageTcpListener(redirectUri.Port))
        {
            Uri authCode = null;
            var listenerTask = listener.ListenToSingleRequestAndRespondAsync(u => {
                authCode = u;
                
                return @"
<html>
<body>
    <p>Successfully Authenticated, you may now close this window</p>
</body>
</html>";
            }, System.Threading.CancellationToken.None);

            var ps = new ProcessStartInfo(authorizationUri.ToString())
            { 
                UseShellExecute = true, 
                Verb = "open" 
            };
            Process.Start(ps);

            await listenerTask;

            return authCode;
        }            
    }
}

因为我已将重定向设置回 localhost,并且此代码位于控制台应用程序中,所以我需要在端口上侦听响应并在应用程序中捕获它,然后向浏览器显示一个值以指示它一切正常。

为了监听端口,我使用了一个从MS Github 抄袭的监听器类:

SingleMessageTcpListener

/// <summary>
/// This object is responsible for listening to a single TCP request, on localhost:port, 
/// extracting the uri, parsing 
/// </summary>
/// <remarks>
/// The underlying TCP listener might capture multiple requests, but only the first one is handled.
///
/// Cribbed this class from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/9e0f57b53edfdcf027cbff401d3ca6c02e95ef1b/tests/devapps/NetCoreTestApp/Experimental/SingleMessageTcpListener.cs
/// </remarks>
internal class SingleMessageTcpListener : IDisposable
{
    private readonly int _port;
    private readonly System.Net.Sockets.TcpListener _tcpListener;

    public SingleMessageTcpListener(int port)
    {
        if (port < 1 || port == 80)
        {
            throw new ArgumentOutOfRangeException("Expected a valid port number, > 0, not 80");
        }

        _port = port;
        _tcpListener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, _port);
        

    }

    public async Task ListenToSingleRequestAndRespondAsync(
        Func<Uri, string> responseProducer,
        CancellationToken cancellationToken)
    {
        cancellationToken.Register(() => _tcpListener.Stop());
        _tcpListener.Start();

        TcpClient tcpClient = null;
        try
        {
            tcpClient =
                await AcceptTcpClientAsync(cancellationToken)
                .ConfigureAwait(false);

            await ExtractUriAndRespondAsync(tcpClient, responseProducer, cancellationToken).ConfigureAwait(false);

        }
        finally
        {
            tcpClient?.Close();
        }
    }

    /// <summary>
    /// AcceptTcpClientAsync does not natively support cancellation, so use this wrapper. Make sure
    /// the cancellation token is registered to stop the listener.
    /// </summary>
    /// <remarks>See https://stackoverflow.com/questions/19220957/tcplistener-how-to-stop-listening-while-awaiting-accepttcpclientasync</remarks>
    private async Task<TcpClient> AcceptTcpClientAsync(CancellationToken token)
    {
        try
        {
            return await _tcpListener.AcceptTcpClientAsync().ConfigureAwait(false);
        }
        catch (Exception ex) when (token.IsCancellationRequested)
        {
            throw new OperationCanceledException("Cancellation was requested while awaiting TCP client connection.", ex);
        }
    }

    private async Task ExtractUriAndRespondAsync(
        TcpClient tcpClient,
        Func<Uri, string> responseProducer,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        string httpRequest = await GetTcpResponseAsync(tcpClient, cancellationToken).ConfigureAwait(false);
        Uri uri = ExtractUriFromHttpRequest(httpRequest);

        // write an "OK, please close the browser message" 
        await WriteResponseAsync(responseProducer(uri), tcpClient.GetStream(), cancellationToken)
            .ConfigureAwait(false);
    }

    private Uri ExtractUriFromHttpRequest(string httpRequest)
    {
        string regexp = @"GET \/\?(.*) HTTP";
        string getQuery = null;
        Regex r1 = new Regex(regexp);
        Match match = r1.Match(httpRequest);
        if (!match.Success)
        {
            throw new InvalidOperationException("Not a GET query");
        }

        getQuery = match.Groups[1].Value;
        UriBuilder uriBuilder = new UriBuilder();
        uriBuilder.Query = getQuery;
        uriBuilder.Port = _port;

        return uriBuilder.Uri;
    }

    private static async Task<string> GetTcpResponseAsync(TcpClient client, CancellationToken cancellationToken)
    {
        NetworkStream networkStream = client.GetStream();

        byte[] readBuffer = new byte[1024];
        StringBuilder stringBuilder = new StringBuilder();
        int numberOfBytesRead = 0;

        // Incoming message may be larger than the buffer size. 
        do
        {
            numberOfBytesRead = await networkStream.ReadAsync(readBuffer, 0, readBuffer.Length, cancellationToken)
                .ConfigureAwait(false);

            string s = Encoding.ASCII.GetString(readBuffer, 0, numberOfBytesRead);
            stringBuilder.Append(s);

        }
        while (networkStream.DataAvailable);

        return stringBuilder.ToString();
    }

    private async Task WriteResponseAsync(
        string message,
        NetworkStream stream,
        CancellationToken cancellationToken)
    {
        string fullResponse = $"HTTP/1.1 200 OK\r\n\r\n{message}";
        var response = Encoding.ASCII.GetBytes(fullResponse);
        await stream.WriteAsync(response, 0, response.Length, cancellationToken).ConfigureAwait(false);
        await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
    }

    public void Dispose()
    {
        _tcpListener?.Stop();
    }
}

所有这些都准备就绪后,浏览器会在连接到资源上的第一个数据库时打开,并且在连接之间重复使用令牌。

【讨论】:

  • 感谢您为此发布全面的答案!
猜你喜欢
  • 1970-01-01
  • 2021-11-11
  • 1970-01-01
  • 2021-05-28
  • 2019-03-30
  • 2014-04-11
  • 1970-01-01
  • 2019-03-21
  • 1970-01-01
相关资源
最近更新 更多