diff --git a/.github/workflows/parameters_aws_auth_tests.json.gpg b/.github/workflows/parameters_aws_auth_tests.json.gpg new file mode 100644 index 000000000..6143d3b24 --- /dev/null +++ b/.github/workflows/parameters_aws_auth_tests.json.gpg @@ -0,0 +1,3 @@ +  Ptn4"Kx{3qr*V}h IV_F-я[qx/,0Njp:k~0;0fpttQF)qv5s٨>P~TyG;Jbo/S uNanlNtGz-T hang_webserver.out 2>&1 popd echo [INFO] Testing -cmd /c node_modules\.bin\mocha --timeout %TIMEOUT% --recursive --full-trace --color --reporter spec test/**/*.js +cmd /c node_modules\.bin\mocha --timeout %TIMEOUT% --recursive --full-trace --color --reporter spec \"test/{unit,integration}/**/*.js\" if %ERRORLEVEL% NEQ 0 ( echo [ERROR] failed to run mocha exit /b 1 diff --git a/package.json b/package.json index 436ced43d..20e7df34d 100644 --- a/package.json +++ b/package.json @@ -61,14 +61,15 @@ "lint:check:all:errorsOnly": "npm run lint:check:all -- --quiet", "lint:fix": "eslint --fix", "test": "mocha -timeout 180000 --recursive --full-trace test/unit/**/*.js test/unit/*.js", + "test:authentication": "mocha --exit -timeout 180000 --recursive --full-trace test/authentication/**/*.js test/authentication/*.js", "test:integration": "mocha -timeout 180000 --recursive --full-trace test/integration/**/*.js test/integration/*.js", "test:single": "mocha -timeout 180000 --full-trace", "test:system": "mocha -timeout 180000 --recursive --full-trace system_test/*.js", "test:unit": "mocha -timeout 180000 --recursive --full-trace test/unit/**/*.js test/unit/*.js", "test:unit:coverage": "nyc npm run test:unit", - "test:ci": "mocha -timeout 180000 --recursive --full-trace test/**/*.js", + "test:ci": "mocha -timeout 180000 --recursive --full-trace 'test/{unit,integration}/**/*.js'", "test:ci:coverage": "nyc npm run test:ci", - "test:ci:withSystemTests": "mocha -timeout 180000 --recursive --full-trace test/**/*.js system_test/*.js", + "test:ci:withSystemTests": "mocha -timeout 180000 --recursive --full-trace 'test/{unit,integration}/**/*.js' system_test/*.js", "test:ci:withSystemTests:coverage": "nyc npm run test:ci:withSystemTests", "test:manual": "mocha -timeout 180000 --full-trace --full-trace test/integration/testManualConnection.js" }, diff --git a/test/authentication/connectionParameters.js b/test/authentication/connectionParameters.js new file mode 100644 index 000000000..620285674 --- /dev/null +++ b/test/authentication/connectionParameters.js @@ -0,0 +1,30 @@ +const snowflakeAuthTestProtocol = process.env.SNOWFLAKE_AUTH_TEST_PROTOCOL; +const snowflakeAuthTestHost = process.env.SNOWFLAKE_AUTH_TEST_HOST; +const snowflakeAuthTestPort = process.env.SNOWFLAKE_AUTH_TEST_PORT; +const snowflakeAuthTestAccount = process.env.SNOWFLAKE_AUTH_TEST_ACCOUNT; +const snowflakeAuthTestRole = process.env.SNOWFLAKE_AUTH_TEST_ROLE; +const snowflakeTestBrowserUser = process.env.SNOWFLAKE_AUTH_TEST_BROWSER_USER; +const snowflakeAuthTestOktaPass = process.env.SNOWFLAKE_AUTH_TEST_OKTA_PASS; +const snowflakeAuthTestDatabase = process.env.SNOWFLAKE_AUTH_TEST_DATABASE; +const snowflakeAuthTestWarehouse = process.env.SNOWFLAKE_AUTH_TEST_WAREHOUSE; +const snowflakeAuthTestSchema = process.env.SNOWFLAKE_AUTH_TEST_SCHEMA; + +const accessUrlAuthTests = snowflakeAuthTestProtocol + '://' + snowflakeAuthTestHost + ':' + + snowflakeAuthTestPort; + +const externalBrowser = + { + accessUrl: accessUrlAuthTests, + username: snowflakeTestBrowserUser, + account: snowflakeAuthTestAccount, + role: snowflakeAuthTestRole, + host: snowflakeAuthTestHost, + warehouse: snowflakeAuthTestWarehouse, + database: snowflakeAuthTestDatabase, + schema: snowflakeAuthTestSchema, + authenticator: 'EXTERNALBROWSER' + }; + +exports.externalBrowser = externalBrowser; +exports.snowflakeTestBrowserUser = snowflakeTestBrowserUser; +exports.snowflakeAuthTestOktaPass = snowflakeAuthTestOktaPass; diff --git a/test/authentication/testExternalBrowser.js b/test/authentication/testExternalBrowser.js new file mode 100644 index 000000000..3ab3ef75b --- /dev/null +++ b/test/authentication/testExternalBrowser.js @@ -0,0 +1,203 @@ +const snowflake = require('../../lib/snowflake'); +const assert = require('assert'); +const testUtil = require('../integration/testUtil'); +const connParameters = require('./connectionParameters'); +const { spawn } = require('child_process'); +const Util = require('../../lib/util'); +const JsonCredentialManager = require('../../lib/authentication/secure_storage/json_credential_manager'); + +describe('External browser authentication tests', function () { + const cleanBrowserProcessesPath = '/externalbrowser/cleanBrowserProcesses.js'; + const provideBrowserCredentialsPath = '/externalbrowser/provideBrowserCredentials.js'; + const login = connParameters.snowflakeTestBrowserUser; + const password = connParameters.snowflakeAuthTestOktaPass; + let connection, error, callbackCompleted; + + before(async () => { + await cleanBrowserProcesses(); + }); + + afterEach(async () => { + await cleanBrowserProcesses(); + await destroyConnection(connection); + callbackCompleted = false; + error = undefined; + }); + + describe('External browser tests', async () => { + it('Successful connection', async () => { + const connectionOption = { ...connParameters.externalBrowser, clientStoreTemporaryCredential: false }; + connection = await snowflake.createConnection(connectionOption); + const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); + await connectAndProvideCredentials(connection, provideCredentialsPromise); + verifyNoErrorWasThrown(); + await verifyConnectionIsUp(connection); + }); + + it('Mismatched Username', async () => { + const connectionOption = { ...connParameters.externalBrowser, username: 'differentUsername', clientStoreTemporaryCredential: false }; + connection = await snowflake.createConnection(connectionOption); + const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); + await connectAndProvideCredentials(connection, provideCredentialsPromise); + assert.strictEqual(error?.message, 'The user you were trying to authenticate as differs from the user currently logged in at the IDP.'); + await verifyConnectionIsNotUp(connection, 'Unable to perform operation using terminated connection.'); + }); + + it('Wrong credentials', async () => { + const login = 'itsnotanaccount.com'; + const password = 'fakepassword'; + const connectionOption = { ...connParameters.externalBrowser, browserActionTimeout: 10000, clientStoreTemporaryCredential: false }; + connection = await snowflake.createConnection(connectionOption); + const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'fail', login, password]); + await connectAndProvideCredentials(connection, provideCredentialsPromise); + assert.strictEqual(error?.message, 'Error while getting SAML token: Browser action timed out after 10000 ms.'); + await verifyConnectionIsNotUp(connection); + }); + + it('External browser timeout', async () => { + const connectionOption = { ...connParameters.externalBrowser, browserActionTimeout: 100, clientStoreTemporaryCredential: false }; + connection = await snowflake.createConnection(connectionOption); + const connectToBrowserPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'timeout']); + await connectAndProvideCredentials(connection, connectToBrowserPromise); + assert.strictEqual(error?.message, 'Error while getting SAML token: Browser action timed out after 100 ms.'); + await verifyConnectionIsNotUp(connection); + }); + }); + + describe('ID Token authentication tests', async () => { + const connectionOption = { ...connParameters.externalBrowser, clientStoreTemporaryCredential: true }; + const key = Util.buildCredentialCacheKey(connectionOption.host, connectionOption.username, 'ID_TOKEN'); + const defaultCredentialManager = new JsonCredentialManager(); + let firstIdToken; + + before(async () => { + await defaultCredentialManager.remove(key); + }); + + it('obtains the id token from the server and saves it on the local storage', async function () { + connection = snowflake.createConnection(connectionOption); + const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); + await connectAndProvideCredentials(connection, provideCredentialsPromise); + verifyNoErrorWasThrown(); + await verifyConnectionIsUp(connection); + }); + + it('the token is saved in the credential manager', async function () { + firstIdToken = await defaultCredentialManager.read(key); + assert.notStrictEqual(firstIdToken, null); + }); + + it('authenticates by token, browser credentials not needed', async function () { + connection = snowflake.createConnection(connectionOption); + await connection.connectAsync(connectAsyncCallback()); + verifyNoErrorWasThrown(); + await verifyConnectionIsUp(connection); + }); + + it('opens browser okta authentication again when token is incorrect', async function () { + await defaultCredentialManager.write(key, '1234'); + connection = snowflake.createConnection(connectionOption); + const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); + await connectAndProvideCredentials(connection, provideCredentialsPromise); + verifyNoErrorWasThrown(); + await verifyConnectionIsUp(connection); + }); + + it('refreshes the token for credential cache key', async function () { + const newToken = await defaultCredentialManager.read(key); + assert.notStrictEqual(firstIdToken, newToken); + }); + }); + + function connectAsyncCallback() { + return function (err) { + error = err; + callbackCompleted = true; + }; + } + + function verifyNoErrorWasThrown() { + assert.equal(error, null); + } + + async function cleanBrowserProcesses() { + if (process.env.RUN_AUTH_TESTS_MANUALLY !== 'true') { + await execWithTimeout('node', [cleanBrowserProcessesPath], 15000); + } + } + + async function connectAndProvideCredentials(connection, provideCredentialsPromise) { + if (process.env.RUN_AUTH_TESTS_MANUALLY === 'true') { + await connection.connectAsync(connectAsyncCallback()); + } else { + await Promise.allSettled([connection.connectAsync(connectAsyncCallback()), provideCredentialsPromise]); + } + await waitForCallbackCompletion(); + } + + async function waitForCallbackCompletion() { + const timeout = Date.now() + 5000; + while (Date.now() < timeout) { + await new Promise(resolve => setTimeout(resolve, 100)); + if (callbackCompleted) { + return; + } + } + throw new Error('Connection callback did not complete'); + } +}); + +async function verifyConnectionIsUp(connection) { + assert.ok(await connection.isValidAsync(), 'Connection is not valid'); + await testUtil.executeCmdAsync(connection, 'Select 1'); +} + +async function verifyConnectionIsNotUp(connection, message = 'Unable to perform operation because a connection was never established.') { + assert.ok(!(connection.isUp()), 'Connection should not be up'); + try { + await testUtil.executeCmdAsync(connection, 'Select 1'); + assert.fail('Expected error was not thrown'); + } catch (error) { + assert.strictEqual(error.message, message); + } +} + +async function destroyConnection(connection) { + if (connection !== undefined && connection.isUp()) { + await testUtil.destroyConnectionAsync(connection); + } +} + +function execWithTimeout(command, args, timeout = 5000) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { shell: true }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => { + stdout += data; + }); + + child.stderr.on('data', (data) => { + stderr += data; + }); + + child.on('error', (err) => { + reject(err); + }); + + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Process exited with code: ${code}, error: ${stderr}`)); + } else { + resolve({ stdout, stderr }); + } + }); + + setTimeout(() => { + child.kill(); + reject(new Error('Process timed out')); + }, timeout); + }); +} diff --git a/test/integration/connectionOptions.js b/test/integration/connectionOptions.js index 38c0ee623..28185ff2e 100644 --- a/test/integration/connectionOptions.js +++ b/test/integration/connectionOptions.js @@ -16,7 +16,6 @@ const snowflakeTestRole = process.env.SNOWFLAKE_TEST_ROLE; const snowflakeTestPassword = process.env.SNOWFLAKE_TEST_PASSWORD; const snowflakeTestAdminUser = process.env.SNOWFLAKE_TEST_ADMIN_USER; const snowflakeTestAdminPassword = process.env.SNOWFLAKE_TEST_ADMIN_PASSWORD; -const snowflakeTestBrowserUser = process.env.SNOWFLAKE_TEST_BROWSER_USER; const snowflakeTestPrivateKeyUser = process.env.SNOWFLAKE_JWT_TEST_USER; const snowflakeTestPrivateKey = process.env.SNOWFLAKE_TEST_PRIVATE_KEY; const snowflakeTestPrivateKeyPath = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_PATH; @@ -89,32 +88,6 @@ const wrongPwd = account: snowflakeTestAccount }; -const externalBrowser = -{ - accessUrl: accessUrl, - username: snowflakeTestBrowserUser, - account: snowflakeTestAccount, - warehouse: snowflakeTestWarehouse, - database: snowflakeTestDatabase, - schema: snowflakeTestSchema, - role: snowflakeTestRole, - host: snowflakeTestHost, - authenticator: 'EXTERNALBROWSER' -}; - -const externalBrowserWithShortTimeout = { - ...externalBrowser, - browserActionTimeout: 100, -}; - -const externalBrowserMismatchUser = -{ - accessUrl: accessUrl, - username: 'node', - account: snowflakeTestAccount, - authenticator: 'EXTERNALBROWSER' -}; - const keypairPrivateKey = { accessUrl: accessUrl, @@ -233,9 +206,6 @@ exports.wrongUserName = wrongUserName; exports.wrongPwd = wrongPwd; exports.accessUrl = accessUrl; exports.account = snowflakeTestAccount; -exports.externalBrowser = externalBrowser; -exports.externalBrowserWithShortTimeout = externalBrowserWithShortTimeout; -exports.externalBrowserMismatchUser = externalBrowserMismatchUser; exports.keypairPrivateKey = keypairPrivateKey; exports.keypairPathEncrypted = keypairPathEncrypted; exports.keypairPathUnencrypted = keypairPathUnencrypted; diff --git a/test/integration/testManualConnection.js b/test/integration/testManualConnection.js index ff375850f..57f148412 100644 --- a/test/integration/testManualConnection.js +++ b/test/integration/testManualConnection.js @@ -13,143 +13,6 @@ const JsonCredentialManager = require('../../lib/authentication/secure_storage/j if (process.env.RUN_MANUAL_TESTS_ONLY === 'true') { describe('Run manual tests', function () { - describe('Connection test - external browser', function () { - it('Simple Connect', function (done) { - const connection = snowflake.createConnection( - connOption.externalBrowser - ); - - connection.connectAsync(function (err, connection) { - try { - assert.ok(connection.isUp(), 'not active'); - testUtil.destroyConnection(connection, function () { - try { - assert.ok(!connection.isUp(), 'not active'); - done(); - } catch (err) { - done(err); - } - }); - } catch (err) { - done(err); - } - }); - }); - - it('Connect - external browser timeout', function (done) { - const connection = snowflake.createConnection( - connOption.externalBrowserWithShortTimeout - ); - - connection.connectAsync(function (err) { - try { - const browserActionTimeout = - connOption.externalBrowserWithShortTimeout.browserActionTimeout; - assert.ok( - err, - `Browser action timed out after ${browserActionTimeout} ms.` - ); - done(); - } catch (err) { - done(err); - } - }); - }); - - it('Mismatched Username', function (done) { - const connection = snowflake.createConnection( - connOption.externalBrowserMismatchUser - ); - connection.connectAsync(function (err) { - try { - assert.ok( - err, - 'Logged in with different user than one on connection string' - ); - assert.equal( - 'The user you were trying to authenticate as differs from the user currently logged in at the IDP.', - err['message'] - ); - done(); - } catch (err) { - done(err); - } - }); - }); - }); - - describe('Connection - ID Token authenticator', function () { - const connectionOption = { ...connOption.externalBrowser, clientStoreTemporaryCredential: true }; - const key = Util.buildCredentialCacheKey(connectionOption.host, connectionOption.username, 'ID_TOKEN'); - const defaultCredentialManager = new JsonCredentialManager(); - let oldToken; - before( async () => { - await defaultCredentialManager.remove(key); - }); - - it('test - obtain the id token from the server and save it on the local storage', function (done) { - const connection = snowflake.createConnection(connectionOption); - connection.connectAsync(function (err) { - try { - assert.ok(!err); - done(); - } catch (err){ - done(err); - } - }); - }); - - it('test - the token is saved in the credential manager correctly', function (done) { - defaultCredentialManager.read(key).then((idToken) => { - try { - oldToken = idToken; - assert.notStrictEqual(idToken, null); - done(); - } catch (err){ - done(err); - } - }); - }); - - - // Web Browser should not be open. - it('test - id token authentication', function (done) { - const idTokenConnection = snowflake.createConnection(connectionOption); - try { - idTokenConnection.connectAsync(function (err) { - assert.ok(!err); - done(); - }); - } catch (err) { - done(err); - } - }); - - // Web Browser should be open. - it('test - id token reauthentication', function (done) { - defaultCredentialManager.write(key, '1234').then(() => { - const wrongTokenConnection = snowflake.createConnection(connectionOption); - wrongTokenConnection.connectAsync(function (err) { - assert.ok(!err); - done(); - }); - }); - }); - - //Compare two idToken. Those two should be different. - it('test - the token is refreshed', function (done) { - oldToken = undefined; - defaultCredentialManager.read(key).then((idToken) => { - try { - assert.notStrictEqual(idToken, oldToken); - done(); - } catch (err) { - done(err); - } - }); - }); - }); - describe('Connection test - oauth', function () { it('Simple Connect', function (done) { const connection = snowflake.createConnection(connOption.oauth);