Подключение к нескольким Azure БД с учетными данными AAD / MFA - PullRequest
1 голос
/ 09 июля 2020

Я пытаюсь написать консольное приложение netcore, которое подключается к нескольким базам данных Azure SQL и выполняет некоторые сценарии против них. Наша компания требует Azure AD с логинами MFA для баз данных.

Мне удалось получить его для успешного входа, используя информацию здесь :

Настройка

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.

1 Ответ

1 голос
/ 10 июля 2020

Итак, в коде есть немного ПЕБКА C. Хотя он использует builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;, на самом деле класс пытается использовать ActiveDirectoryIntegrated. Так что мой класс AD никогда не был поражен. Кроме того, в примере кода это на самом деле никогда не сработало бы, потому что оператор case существует для ActiveDirectoryIntegrated - я вырезал его в своей локальной копии.

Мне действительно нужно было использовать правильный 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, и этот код живет внутри консольного приложения, мне нужно прослушать порт для ответа и записать его в приложении, а затем отобразить значение в браузере, чтобы указать, что все работает.

Для прослушивания порта я использовал слушатель класса c ребристый из 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();
    }
}

При всем этом браузер открывается при подключении к первой базе данных на ресурс, и токен повторно используется между соединениями.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...