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

SNOW-955536: Multiple SAML Integration #852

Merged
merged 4 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,50 @@ public void TestSSOConnectionWithUserAsync()
}
}

[Test]
[Ignore("This test requires manual interaction and therefore cannot be run in CI")]
public void TestSSOConnectionWithUserAndDisableConsoleLogin()
{
// Use external browser to log in using proper password for [email protected]
using (IDbConnection conn = new SnowflakeDbConnection())
{
conn.ConnectionString
= ConnectionStringWithoutAuth
+ ";authenticator=externalbrowser;[email protected];disable_console_login=false;";
conn.Open();
Assert.AreEqual(ConnectionState.Open, conn.State);
using (IDbCommand command = conn.CreateCommand())
{
command.CommandText = "SELECT CURRENT_USER()";
Assert.AreEqual("QA", command.ExecuteScalar().ToString());
}
}
}

[Test]
[Ignore("This test requires manual interaction and therefore cannot be run in CI")]
public void TestSSOConnectionWithUserAsyncAndDisableConsoleLogin()
{
// Use external browser to log in using proper password for [email protected]
using (SnowflakeDbConnection conn = new SnowflakeDbConnection())
{
conn.ConnectionString
= ConnectionStringWithoutAuth
+ ";authenticator=externalbrowser;[email protected];disable_console_login=false;";

Task connectTask = conn.OpenAsync(CancellationToken.None);
connectTask.Wait();
Assert.AreEqual(ConnectionState.Open, conn.State);
using (DbCommand command = conn.CreateCommand())
{
command.CommandText = "SELECT CURRENT_USER()";
Task<object> task = command.ExecuteScalarAsync(CancellationToken.None);
task.Wait(CancellationToken.None);
Assert.AreEqual("QA", task.Result);
}
}
}

[Test]
[Ignore("This test requires manual interaction and therefore cannot be run in CI")]
public void TestSSOConnectionTimeoutAfter10s()
Expand Down
42 changes: 42 additions & 0 deletions Snowflake.Data.Tests/UnitTests/SFSessionPropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
string defMaxHttpRetries = "7";
string defIncludeRetryReason = "true";
string defDisableQueryContextCache = "false";
string defDisableConsoleLogin = "true";
string defAllowUnderscoresInHost = "false";

var simpleTestCase = new TestCase()
Expand Down Expand Up @@ -105,6 +106,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};
Expand Down Expand Up @@ -132,6 +134,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};
Expand Down Expand Up @@ -162,6 +165,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
},
ConnectionString =
Expand Down Expand Up @@ -194,6 +198,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
},
ConnectionString =
Expand Down Expand Up @@ -225,6 +230,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.FILE_TRANSFER_MEMORY_THRESHOLD, "25" },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};
Expand Down Expand Up @@ -253,6 +259,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, "false" },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};
Expand Down Expand Up @@ -280,11 +287,42 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, "true" },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
},
ConnectionString =
$"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLEQUERYCONTEXTCACHE=true"
};
var testCaseWithDisableConsoleLogin = new TestCase()
{
ExpectedProperties = new SFSessionProperties()
{
{ SFSessionProperty.ACCOUNT, defAccount },
{ SFSessionProperty.USER, defUser },
{ SFSessionProperty.HOST, defHost },
{ SFSessionProperty.AUTHENTICATOR, defAuthenticator },
{ SFSessionProperty.SCHEME, defScheme },
{ SFSessionProperty.CONNECTION_TIMEOUT, defConnectionTimeout },
{ SFSessionProperty.PASSWORD, defPassword },
{ SFSessionProperty.PORT, defPort },
{ SFSessionProperty.VALIDATE_DEFAULT_PARAMETERS, "true" },
{ SFSessionProperty.USEPROXY, "false" },
{ SFSessionProperty.INSECUREMODE, "false" },
{ SFSessionProperty.DISABLERETRY, "false" },
{ SFSessionProperty.FORCERETRYON404, "false" },
{ SFSessionProperty.CLIENT_SESSION_KEEP_ALIVE, "false" },
{ SFSessionProperty.FORCEPARSEERROR, "false" },
{ SFSessionProperty.BROWSER_RESPONSE_TIMEOUT, defBrowserResponseTime },
{ SFSessionProperty.RETRY_TIMEOUT, defRetryTimeout },
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, "false" },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
},
ConnectionString =
$"ACCOUNT={defAccount};USER={defUser};PASSWORD={defPassword};DISABLE_CONSOLE_LOGIN=false"
};
var complicatedAccount = $"{defAccount}.region-name.host-name";
var testCaseComplicatedAccountName = new TestCase()
{
Expand All @@ -311,6 +349,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};
Expand Down Expand Up @@ -339,6 +378,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, defAllowUnderscoresInHost }
}
};
Expand Down Expand Up @@ -367,6 +407,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
{ SFSessionProperty.MAXHTTPRETRIES, defMaxHttpRetries },
{ SFSessionProperty.INCLUDERETRYREASON, defIncludeRetryReason },
{ SFSessionProperty.DISABLEQUERYCONTEXTCACHE, defDisableQueryContextCache },
{ SFSessionProperty.DISABLE_CONSOLE_LOGIN, defDisableConsoleLogin },
{ SFSessionProperty.ALLOWUNDERSCORESINHOST, "true" }
}
};
Expand All @@ -379,6 +420,7 @@ public static IEnumerable<TestCase> ConnectionStringTestCases()
testCaseWithFileTransferMaxBytesInMemory,
testCaseWithIncludeRetryReason,
testCaseWithDisableQueryContextCache,
testCaseWithDisableConsoleLogin,
testCaseComplicatedAccountName,
sfc-gh-dstempniak marked this conversation as resolved.
Show resolved Hide resolved
testCaseUnderscoredAccountName,
testCaseUnderscoredAccountNameWithEnabledAllowUnderscores
Expand Down
73 changes: 56 additions & 17 deletions Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Snowflake.Data.Log;
using Snowflake.Data.Client;
using System.Text.RegularExpressions;
using System.Collections.Generic;

namespace Snowflake.Data.Core.Authenticator
{
Expand Down Expand Up @@ -54,19 +55,28 @@
httpListener.Start();

logger.Debug("Get IdpUrl and ProofKey");
var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort);
var authenticatorRestResponse =
await session.restRequester.PostAsync<AuthenticatorResponse>(
authenticatorRestRequest,
cancellationToken
).ConfigureAwait(false);
authenticatorRestResponse.FilterFailedResponse();
string loginUrl;
if (session._disableConsoleLogin)
{
var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort);
var authenticatorRestResponse =
await session.restRequester.PostAsync<AuthenticatorResponse>(
authenticatorRestRequest,
cancellationToken
).ConfigureAwait(false);
authenticatorRestResponse.FilterFailedResponse();

Check warning on line 67 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L60-L67

Added lines #L60 - L67 were not covered by tests

var idpUrl = authenticatorRestResponse.data.ssoUrl;
_proofKey = authenticatorRestResponse.data.proofKey;
loginUrl = authenticatorRestResponse.data.ssoUrl;
_proofKey = authenticatorRestResponse.data.proofKey;
}

Check warning on line 71 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L69-L71

Added lines #L69 - L71 were not covered by tests
else
{
_proofKey = GenerateProofKey();
loginUrl = GetLoginUrl(_proofKey, localPort);
}

Check warning on line 76 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L73-L76

Added lines #L73 - L76 were not covered by tests

logger.Debug("Open browser");
StartBrowser(idpUrl);
StartBrowser(loginUrl);

Check warning on line 79 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L79

Added line #L79 was not covered by tests

logger.Debug("Get the redirect SAML request");
_successEvent = new ManualResetEvent(false);
Expand Down Expand Up @@ -96,15 +106,24 @@
httpListener.Start();

logger.Debug("Get IdpUrl and ProofKey");
var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort);
var authenticatorRestResponse = session.restRequester.Post<AuthenticatorResponse>(authenticatorRestRequest);
authenticatorRestResponse.FilterFailedResponse();
string loginUrl;
if (session._disableConsoleLogin)
{
var authenticatorRestRequest = BuildAuthenticatorRestRequest(localPort);
var authenticatorRestResponse = session.restRequester.Post<AuthenticatorResponse>(authenticatorRestRequest);
authenticatorRestResponse.FilterFailedResponse();

Check warning on line 114 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L111-L114

Added lines #L111 - L114 were not covered by tests

var idpUrl = authenticatorRestResponse.data.ssoUrl;
_proofKey = authenticatorRestResponse.data.proofKey;
loginUrl = authenticatorRestResponse.data.ssoUrl;
_proofKey = authenticatorRestResponse.data.proofKey;
}

Check warning on line 118 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L116-L118

Added lines #L116 - L118 were not covered by tests
else
{
_proofKey = GenerateProofKey();
loginUrl = GetLoginUrl(_proofKey, localPort);
}

Check warning on line 123 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L120-L123

Added lines #L120 - L123 were not covered by tests

logger.Debug("Open browser");
StartBrowser(idpUrl);
StartBrowser(loginUrl);

Check warning on line 126 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L126

Added line #L126 was not covered by tests

logger.Debug("Get the redirect SAML request");
_successEvent = new ManualResetEvent(false);
Expand Down Expand Up @@ -187,7 +206,7 @@
// The following code is learnt from https://brockallen.com/2016/09/24/process-start-for-urls-on-net-core/
#if NETFRAMEWORK
// .net standard would pass here
Process.Start(url);
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });

Check warning on line 209 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L209

Added line #L209 was not covered by tests
sfc-gh-dstempniak marked this conversation as resolved.
Show resolved Hide resolved
#else
// hack because of this: https://github.com/dotnet/corefx/issues/10361
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand Down Expand Up @@ -247,5 +266,25 @@
data.Token = _samlResponseToken;
data.ProofKey = _proofKey;
}

private string GetLoginUrl(string proofKey, int localPort)
{
Dictionary<string, string> parameters = new Dictionary<string, string>()
{
{ "login_name", session.properties[SFSessionProperty.USER]},
{ "proof_key", proofKey },
{ "browser_mode_redirect_port", localPort.ToString() }
};
Uri loginUrl = session.BuildUri(RestPath.SF_CONSOLE_LOGIN, parameters);
return loginUrl.ToString();
}

Check warning on line 280 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L271-L280

Added lines #L271 - L280 were not covered by tests

private string GenerateProofKey()
{
Random rnd = new Random();
Byte[] randomness = new Byte[32];
rnd.NextBytes(randomness);
return Convert.ToBase64String(randomness);
}

Check warning on line 288 in Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs

View check run for this annotation

Codecov / codecov/patch

Snowflake.Data/Core/Authenticator/ExternalBrowserAuthenticator.cs#L283-L288

Added lines #L283 - L288 were not covered by tests
}
}
2 changes: 2 additions & 0 deletions Snowflake.Data/Core/RestParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ internal static class RestPath
internal const string SF_QUERY_PATH = "/queries/v1/query-request";

internal const string SF_SESSION_HEARTBEAT_PATH = SF_SESSION_PATH + "/heartbeat";

internal const string SF_CONSOLE_LOGIN = "/console/login";
}

internal class SFEnvironment
Expand Down
3 changes: 3 additions & 0 deletions Snowflake.Data/Core/Session/SFSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public class SFSession

private bool _disableQueryContextCache = false;

internal bool _disableConsoleLogin;

internal void ProcessLoginResponse(LoginResponse authnResponse)
{
if (authnResponse.success)
Expand Down Expand Up @@ -148,6 +150,7 @@ internal SFSession(
connStr = connectionString;
properties = SFSessionProperties.parseConnectionString(connectionString, password);
_disableQueryContextCache = bool.Parse(properties[SFSessionProperty.DISABLEQUERYCONTEXTCACHE]);
_disableConsoleLogin = bool.Parse(properties[SFSessionProperty.DISABLE_CONSOLE_LOGIN]);
ValidateApplicationName(properties);
try
{
Expand Down
2 changes: 2 additions & 0 deletions Snowflake.Data/Core/Session/SFSessionProperty.cs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ internal enum SFSessionProperty
DISABLEQUERYCONTEXTCACHE,
[SFSessionPropertyAttr(required = false)]
CLIENT_CONFIG_FILE,
[SFSessionPropertyAttr(required = false, defaultValue = "true")]
DISABLE_CONSOLE_LOGIN,
[SFSessionPropertyAttr(required = false, defaultValue = "false")]
ALLOWUNDERSCORESINHOST
}
Expand Down
Loading