我正在尝试编写一个netcore控制台应用程序,该应用程序可以连接到多个Azure SQL数据库,并针对它们执行一些脚本。我们的公司要求数据库具有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实例上。
答案 0 :(得分:1)
因此,代码中包含一些PEBKAC。尽管该类正在使用builder.Authentication = SqlAuthenticationMethod.ActiveDirectoryInteractive;
,但该类实际上正在尝试使用ActiveDirectoryIntegrated
。因此,我的广告课从未真正受到欢迎。另外,在示例代码中,它实际上也永远不会起作用,因为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;
}
}
这里有些不同:
_redirectURL
属性设置为http://localhost:8089
最后,我创建了自己的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;
}
}
}
因为我已将重定向设置回本地主机,并且此代码位于控制台应用程序中,所以我需要侦听端口的响应并将其捕获到应用程序中,然后向浏览器显示一个值以指示该响应一切正常。
要监听端口,我使用了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();
}
}
所有这些都准备就绪,当连接到资源上的第一个数据库时,浏览器将打开,并且令牌将在连接之间重用。