Skip to content

Commit c45f1ad

Browse files
SNOW-715504: MFA token cache support (#988)
Co-authored-by: Krzysztof Nozderko <[email protected]>
1 parent 27b0ae4 commit c45f1ad

File tree

61 files changed

+2745
-158
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2745
-158
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ jobs:
4141
with:
4242
dotnet-version: |
4343
6.0.x
44+
7.0.x
4445
8.0.x
4546
9.0.x
4647
dotnet-quality: 'ga'
@@ -103,6 +104,7 @@ jobs:
103104
with:
104105
dotnet-version: |
105106
6.0.x
107+
7.0.x
106108
8.0.x
107109
9.0.x
108110
dotnet-quality: 'ga'
@@ -163,6 +165,7 @@ jobs:
163165
with:
164166
dotnet-version: |
165167
6.0.x
168+
7.0.x
166169
8.0.x
167170
9.0.x
168171
dotnet-quality: 'ga'

Snowflake.Data.Tests/IntegrationTests/SFConnectionIT.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Snowflake.Data.Client;
1515
using Snowflake.Data.Core;
1616
using Snowflake.Data.Core.Session;
17+
using Snowflake.Data.Core.Tools;
1718
using Snowflake.Data.Log;
1819
using Snowflake.Data.Tests.Mock;
1920
using Snowflake.Data.Tests.Util;
@@ -2271,6 +2272,52 @@ public void TestUseMultiplePoolsConnectionPoolByDefault()
22712272
Assert.AreEqual(ConnectionPoolType.MultipleConnectionPool, poolVersion);
22722273
}
22732274

2275+
[Test]
2276+
[Ignore("This test requires manual interaction and therefore cannot be run in CI")] // to enroll to mfa authentication edit your user profile
2277+
public void TestMFATokenCachingWithPasscodeFromConnectionString()
2278+
{
2279+
// Use a connection with MFA enabled and set passcode property for mfa authentication. e.g. ConnectionString + ";authenticator=username_password_mfa;passcode=(set proper passcode)"
2280+
// ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account.
2281+
// On Mac/Linux OS the default credential manager is a file based one. Uncomment the following line to test in memory implementation.
2282+
// SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager();
2283+
using (SnowflakeDbConnection conn = new SnowflakeDbConnection())
2284+
{
2285+
conn.ConnectionString
2286+
= ConnectionString
2287+
+ ";authenticator=username_password_mfa;application=DuoTest;minPoolSize=0;passcode=(set proper passcode)";
2288+
2289+
2290+
// Authenticate to retrieve and store the token if doesn't exist or invalid
2291+
Task connectTask = conn.OpenAsync(CancellationToken.None);
2292+
connectTask.Wait();
2293+
Assert.AreEqual(ConnectionState.Open, conn.State);
2294+
}
2295+
}
2296+
2297+
[Test]
2298+
[Ignore("Requires manual steps and environment with mfa authentication enrolled")] // to enroll to mfa authentication edit your user profile
2299+
public void TestMfaWithPasswordConnectionUsingPasscodeWithSecureString()
2300+
{
2301+
// Use a connection with MFA enabled and Passcode property on connection instance.
2302+
// ACCOUNT PARAMETER ALLOW_CLIENT_MFA_CACHING should be set to true in the account.
2303+
// On Mac/Linux OS the default credential manager is a file based one. Uncomment the following line to test in memory implementation.
2304+
// SnowflakeCredentialManagerFactory.UseInMemoryCredentialManager();
2305+
// arrange
2306+
using (SnowflakeDbConnection conn = new SnowflakeDbConnection())
2307+
{
2308+
conn.Passcode = SecureStringHelper.Encode("$(set proper passcode)");
2309+
// manual action: stop here in breakpoint to provide proper passcode by: conn.Passcode = SecureStringHelper.Encode("...");
2310+
conn.ConnectionString = ConnectionString + "minPoolSize=2;application=DuoTest;";
2311+
2312+
// act
2313+
Task connectTask = conn.OpenAsync(CancellationToken.None);
2314+
connectTask.Wait();
2315+
2316+
// assert
2317+
Assert.AreEqual(ConnectionState.Open, conn.State);
2318+
}
2319+
}
2320+
22742321
[Test]
22752322
[TestCase("connection_timeout=5;")]
22762323
[TestCase("")]
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Snowflake.Data.Core;
7+
8+
namespace Snowflake.Data.Tests.Mock
9+
{
10+
using Microsoft.IdentityModel.Tokens;
11+
12+
class MockLoginMFATokenCacheRestRequester: IMockRestRequester
13+
{
14+
internal Queue<LoginRequest> LoginRequests { get; } = new();
15+
16+
internal Queue<LoginResponseData> LoginResponses { get; } = new();
17+
18+
public T Get<T>(IRestRequest request)
19+
{
20+
return Task.Run(async () => await (GetAsync<T>(request, CancellationToken.None)).ConfigureAwait(false)).Result;
21+
}
22+
23+
public Task<T> GetAsync<T>(IRestRequest request, CancellationToken cancellationToken)
24+
{
25+
return Task.FromResult<T>((T)(object)null);
26+
}
27+
28+
public Task<HttpResponseMessage> GetAsync(IRestRequest request, CancellationToken cancellationToken)
29+
{
30+
return Task.FromResult<HttpResponseMessage>(null);
31+
}
32+
33+
public HttpResponseMessage Get(IRestRequest request)
34+
{
35+
return null;
36+
}
37+
38+
public T Post<T>(IRestRequest postRequest)
39+
{
40+
return Task.Run(async () => await (PostAsync<T>(postRequest, CancellationToken.None)).ConfigureAwait(false)).Result;
41+
}
42+
43+
public Task<T> PostAsync<T>(IRestRequest postRequest, CancellationToken cancellationToken)
44+
{
45+
SFRestRequest sfRequest = (SFRestRequest)postRequest;
46+
if (sfRequest.jsonBody is LoginRequest)
47+
{
48+
LoginRequests.Enqueue((LoginRequest) sfRequest.jsonBody);
49+
var responseData = this.LoginResponses.IsNullOrEmpty() ? new LoginResponseData()
50+
{
51+
token = "session_token",
52+
masterToken = "master_token",
53+
authResponseSessionInfo = new SessionInfo(),
54+
nameValueParameter = new List<NameValueParameter>()
55+
} : this.LoginResponses.Dequeue();
56+
var authnResponse = new LoginResponse
57+
{
58+
data = responseData,
59+
success = true
60+
};
61+
62+
// login request return success
63+
return Task.FromResult<T>((T)(object)authnResponse);
64+
}
65+
else if (sfRequest.jsonBody is CloseResponse)
66+
{
67+
var authnResponse = new CloseResponse()
68+
{
69+
success = true
70+
};
71+
72+
// login request return success
73+
return Task.FromResult<T>((T)(object)authnResponse);
74+
}
75+
throw new NotImplementedException();
76+
}
77+
78+
public void setHttpClient(HttpClient httpClient)
79+
{
80+
// Nothing to do
81+
}
82+
83+
public void Reset()
84+
{
85+
LoginRequests.Clear();
86+
LoginResponses.Clear();
87+
}
88+
}
89+
}

Snowflake.Data.Tests/Mock/MockSnowflakeDbConnection.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ public override Task OpenAsync(CancellationToken cancellationToken)
7878
cancellationToken);
7979

8080
}
81-
81+
8282
private void SetMockSession()
8383
{
84-
SfSession = new SFSession(ConnectionString, Password, _restRequester);
84+
SfSession = new SFSession(ConnectionString, Password, Passcode, EasyLoggingStarter.Instance, _restRequester);
8585

8686
_connectionTimeout = (int)SfSession.connectionTimeout.TotalSeconds;
8787

@@ -92,7 +92,7 @@ private void OnSessionEstablished()
9292
{
9393
_connectionState = ConnectionState.Open;
9494
}
95-
95+
9696
protected override bool CanReuseSession(TransactionRollbackStatus transactionRollbackStatus)
9797
{
9898
return false;

0 commit comments

Comments
 (0)