diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b7d430c57..766856111 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,5 +5,6 @@ Please explain the changes you made here. - [ ] Format code according to the existing code style (run `npm run lint:check -- CHANGED_FILES` and fix problems in changed code) - [ ] Create tests which fail without the change (if possible) - [ ] Make all tests (unit and integration) pass (`npm run test:unit` and `npm run test:integration`) +- [ ] Extend the types in index.d.ts file (if necessary) - [ ] Extend the README / documentation and ensure is properly displayed (if necessary) - [ ] Provide JIRA issue id (if possible) or GitHub issue id in commit message diff --git a/.github/workflows/parameters_aws_auth_tests.json.gpg b/.github/workflows/parameters_aws_auth_tests.json.gpg index 6143d3b24..bea557c13 100644 Binary files a/.github/workflows/parameters_aws_auth_tests.json.gpg and b/.github/workflows/parameters_aws_auth_tests.json.gpg differ diff --git a/.github/workflows/rsa_keys/rsa_encrypted_key.p8.gpg b/.github/workflows/rsa_keys/rsa_encrypted_key.p8.gpg new file mode 100644 index 000000000..3643cdb1f Binary files /dev/null and b/.github/workflows/rsa_keys/rsa_encrypted_key.p8.gpg differ diff --git a/.github/workflows/rsa_keys/rsa_key.p8.gpg b/.github/workflows/rsa_keys/rsa_key.p8.gpg new file mode 100644 index 000000000..e90253cd3 Binary files /dev/null and b/.github/workflows/rsa_keys/rsa_key.p8.gpg differ diff --git a/.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg b/.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg new file mode 100644 index 000000000..3d2442a7c Binary files /dev/null and b/.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg differ diff --git a/.gitignore b/.gitignore index 785a1a3c7..5c1e6c21c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ wss-*-agent.config wss-unified-agent.jar whitesource/ .nyc_output +rsa_*.p8 diff --git a/ci/container/test_authentication.sh b/ci/container/test_authentication.sh index cffe7336e..31e18dde5 100755 --- a/ci/container/test_authentication.sh +++ b/ci/container/test_authentication.sh @@ -5,4 +5,8 @@ set -o pipefail AUTH_PARAMETER_FILE=./.github/workflows/parameters_aws_auth_tests.json eval $(jq -r '.authtestparams | to_entries | map("export \(.key)=\(.value|tostring)")|.[]' $AUTH_PARAMETER_FILE) +export SNOWFLAKE_AUTH_TEST_PRIVATE_KEY_PATH=./.github/workflows/rsa_keys/rsa_key.p8 +export SNOWFLAKE_AUTH_TEST_ENCRYPTED_PRIVATE_KEY_PATH=./.github/workflows/rsa_keys/rsa_encrypted_key.p8 +export SNOWFLAKE_AUTH_TEST_INVALID_PRIVATE_KEY_PATH=./.github/workflows/rsa_keys/rsa_key_invalid.p8 + npm run test:authentication diff --git a/ci/test_authentication.sh b/ci/test_authentication.sh index 725520798..49ab3ae26 100755 --- a/ci/test_authentication.sh +++ b/ci/test_authentication.sh @@ -5,10 +5,13 @@ THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export WORKSPACE=${WORKSPACE:-/tmp} gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/parameters_aws_auth_tests.json "$THIS_DIR/../.github/workflows/parameters_aws_auth_tests.json.gpg" +gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/rsa_keys/rsa_encrypted_key.p8 "$THIS_DIR/../.github/workflows/rsa_keys/rsa_encrypted_key.p8.gpg" +gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/rsa_keys/rsa_key.p8 "$THIS_DIR/../.github/workflows/rsa_keys/rsa_key.p8.gpg" +gpg --quiet --batch --yes --decrypt --passphrase="$PARAMETERS_SECRET" --output $THIS_DIR/../.github/workflows/rsa_keys/rsa_key_invalid.p8 "$THIS_DIR/../.github/workflows/rsa_keys/rsa_key_invalid.p8.gpg" docker run \ -v $(cd $THIS_DIR/.. && pwd):/mnt/host \ -v $WORKSPACE:/mnt/workspace \ --rm \ - nexus.int.snowflakecomputing.com:8086/docker/snowdrivers-test-external-browser:2 \ + nexus.int.snowflakecomputing.com:8086/docker/snowdrivers-test-external-browser:3 \ "/mnt/host/ci/container/test_authentication.sh" diff --git a/test/authentication/authTestsBaseClass.js b/test/authentication/authTestsBaseClass.js new file mode 100644 index 000000000..74308399a --- /dev/null +++ b/test/authentication/authTestsBaseClass.js @@ -0,0 +1,69 @@ +const assert = require('assert'); +const testUtil = require('../integration/testUtil'); +const snowflake = require('../../lib/snowflake'); + +class AuthTest { + constructor() { + this.connection = null; + this.error = null; + this.callbackCompleted = false; + } + + connectAsyncCallback() { + return (err) => { + this.error = err; + this.callbackCompleted = true; + }; + } + + async waitForCallbackCompletion() { + const timeout = Date.now() + 5000; + while (Date.now() < timeout) { + await new Promise(resolve => setTimeout(resolve, 100)); + if (this.callbackCompleted) { + return; + } + } + throw new Error('Connection callback did not complete'); + } + + async createConnection(connectionOption) { + this.connection = snowflake.createConnection(connectionOption); + } + + async connectAsync() { + await this.connection.connectAsync(this.connectAsyncCallback()); + await this.waitForCallbackCompletion(); + } + + async verifyConnectionIsUp() { + assert.ok(await this.connection.isValidAsync(), 'Connection is not valid'); + await testUtil.executeCmdAsync(this.connection, 'Select 1'); + } + + async verifyConnectionIsNotUp(message = 'Unable to perform operation because a connection was never established.') { + assert.ok(!(this.connection.isUp()), 'Connection should not be up'); + try { + await testUtil.executeCmdAsync(this.connection, 'Select 1'); + assert.fail('Expected error was not thrown'); + } catch (error) { + assert.strictEqual(error.message, message); + } + } + + async destroyConnection() { + if (this.connection !== undefined && this.connection !== null && this.connection.isUp()) { + await testUtil.destroyConnectionAsync(this.connection); + } + } + + verifyNoErrorWasThrown() { + assert.equal(this.error, null); + } + + verifyErrorWasThrown(message) { + assert.strictEqual(this.error?.message, message); + } +} + +module.exports = AuthTest; diff --git a/test/authentication/connectionParameters.js b/test/authentication/connectionParameters.js index 620285674..7d9d10745 100644 --- a/test/authentication/connectionParameters.js +++ b/test/authentication/connectionParameters.js @@ -3,28 +3,93 @@ 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 snowflakeAuthTestBrowserUser = process.env.SNOWFLAKE_AUTH_TEST_BROWSER_USER; +const snowflakeAuthTestOktaAuth = process.env.SNOWFLAKE_AUTH_TEST_OKTA_AUTH; +const snowflakeAuthTestOktaUser = process.env.SNOWFLAKE_AUTH_TEST_OKTA_USER; const snowflakeAuthTestOktaPass = process.env.SNOWFLAKE_AUTH_TEST_OKTA_PASS; +const snowflakeAuthTestOauthUrl = process.env.SNOWFLAKE_AUTH_TEST_OAUTH_URL; +const snowflakeAuthTestOauthClientId = process.env.SNOWFLAKE_AUTH_TEST_OAUTH_CLIENT_ID; +const snowflakeAuthTestOauthClientSecret = process.env.SNOWFLAKE_AUTH_TEST_OAUTH_CLIENT_SECRET; 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 snowflakeAuthTestPrivateKeyPath = process.env.SNOWFLAKE_AUTH_TEST_PRIVATE_KEY_PATH; +const snowflakeAuthTestInvalidPrivateKeyPath = process.env.SNOWFLAKE_AUTH_TEST_INVALID_PRIVATE_KEY_PATH; +const snowflakeAuthTestPrivateKeyPassword = process.env.SNOWFLAKE_AUTH_TEST_PRIVATE_KEY_PASSWORD; +const snowflakeAuthTestEncryptedPrivateKeyPath = process.env.SNOWFLAKE_AUTH_TEST_ENCRYPTED_PRIVATE_KEY_PATH; const accessUrlAuthTests = snowflakeAuthTestProtocol + '://' + snowflakeAuthTestHost + ':' + - snowflakeAuthTestPort; + snowflakeAuthTestPort; + +const baseParameters = + { + accessUrl: accessUrlAuthTests, + account: snowflakeAuthTestAccount, + role: snowflakeAuthTestRole, + host: snowflakeAuthTestHost, + warehouse: snowflakeAuthTestWarehouse, + database: snowflakeAuthTestDatabase, + schema: snowflakeAuthTestSchema, + }; const externalBrowser = - { - accessUrl: accessUrlAuthTests, - username: snowflakeTestBrowserUser, - account: snowflakeAuthTestAccount, - role: snowflakeAuthTestRole, - host: snowflakeAuthTestHost, - warehouse: snowflakeAuthTestWarehouse, - database: snowflakeAuthTestDatabase, - schema: snowflakeAuthTestSchema, - authenticator: 'EXTERNALBROWSER' - }; + { + ...baseParameters, + username: snowflakeAuthTestBrowserUser, + authenticator: 'EXTERNALBROWSER' + }; + +const okta = + { + ...baseParameters, + username: snowflakeAuthTestOktaUser, + password: snowflakeAuthTestOktaPass, + authenticator: snowflakeAuthTestOktaAuth + }; + +const oauth = + { + ...baseParameters, + username: snowflakeAuthTestOktaUser, + authenticator: 'OAUTH' + }; + +const keypairPrivateKey = + { + ...baseParameters, + username: snowflakeAuthTestOktaUser, + authenticator: 'SNOWFLAKE_JWT' + }; + +const keypairPrivateKeyPath = + { + ...baseParameters, + username: snowflakeAuthTestOktaUser, + privateKeyPath: snowflakeAuthTestPrivateKeyPath, + authenticator: 'SNOWFLAKE_JWT' + }; + +const keypairEncryptedPrivateKeyPath = + { + ...baseParameters, + username: snowflakeAuthTestOktaUser, + privateKeyPass: snowflakeAuthTestPrivateKeyPassword, + privateKeyPath: snowflakeAuthTestEncryptedPrivateKeyPath, + authenticator: 'SNOWFLAKE_JWT' + }; exports.externalBrowser = externalBrowser; -exports.snowflakeTestBrowserUser = snowflakeTestBrowserUser; +exports.okta = okta; +exports.oauth = oauth; +exports.keypairPrivateKey = keypairPrivateKey; +exports.keypairPrivateKeyPath = keypairPrivateKeyPath; +exports.keypairEncryptedPrivateKeyPath = keypairEncryptedPrivateKeyPath; +exports.snowflakeTestBrowserUser = snowflakeAuthTestBrowserUser; +exports.snowflakeAuthTestOktaUser = snowflakeAuthTestOktaUser; exports.snowflakeAuthTestOktaPass = snowflakeAuthTestOktaPass; +exports.snowflakeAuthTestRole = snowflakeAuthTestRole; +exports.snowflakeAuthTestOauthClientId = snowflakeAuthTestOauthClientId; +exports.snowflakeAuthTestOauthClientSecret = snowflakeAuthTestOauthClientSecret; +exports.snowflakeAuthTestOauthUrl = snowflakeAuthTestOauthUrl; +exports.snowflakeAuthTestPrivateKeyPath = snowflakeAuthTestPrivateKeyPath; +exports.snowflakeAuthTestInvalidPrivateKeyPath = snowflakeAuthTestInvalidPrivateKeyPath; diff --git a/test/authentication/testExternalBrowser.js b/test/authentication/testExternalBrowser.js index 3ab3ef75b..ade0f03ce 100644 --- a/test/authentication/testExternalBrowser.js +++ b/test/authentication/testExternalBrowser.js @@ -1,66 +1,64 @@ -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'); +const AuthTest = require('./authTestsBaseClass.js'); describe('External browser authentication tests', function () { + const runAuthTestsManually = process.env.RUN_AUTH_TESTS_MANUALLY === 'true'; const cleanBrowserProcessesPath = '/externalbrowser/cleanBrowserProcesses.js'; const provideBrowserCredentialsPath = '/externalbrowser/provideBrowserCredentials.js'; const login = connParameters.snowflakeTestBrowserUser; const password = connParameters.snowflakeAuthTestOktaPass; - let connection, error, callbackCompleted; + let authTest; - before(async () => { + beforeEach(async () => { + authTest = new AuthTest(); await cleanBrowserProcesses(); }); afterEach(async () => { - await cleanBrowserProcesses(); - await destroyConnection(connection); - callbackCompleted = false; - error = undefined; + await authTest.destroyConnection(); }); describe('External browser tests', async () => { it('Successful connection', async () => { const connectionOption = { ...connParameters.externalBrowser, clientStoreTemporaryCredential: false }; - connection = await snowflake.createConnection(connectionOption); + authTest.createConnection(connectionOption); const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); - await connectAndProvideCredentials(connection, provideCredentialsPromise); - verifyNoErrorWasThrown(); - await verifyConnectionIsUp(connection); + await connectAndProvideCredentials(provideCredentialsPromise); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); }); it('Mismatched Username', async () => { const connectionOption = { ...connParameters.externalBrowser, username: 'differentUsername', clientStoreTemporaryCredential: false }; - connection = await snowflake.createConnection(connectionOption); + authTest.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.'); + await connectAndProvideCredentials(provideCredentialsPromise); + authTest.verifyErrorWasThrown('The user you were trying to authenticate as differs from the user currently logged in at the IDP.'); + await authTest.verifyConnectionIsNotUp('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); + authTest.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); + await connectAndProvideCredentials(provideCredentialsPromise); + authTest.verifyErrorWasThrown('Error while getting SAML token: Browser action timed out after 10000 ms.'); + await authTest.verifyConnectionIsNotUp(); }); it('External browser timeout', async () => { const connectionOption = { ...connParameters.externalBrowser, browserActionTimeout: 100, clientStoreTemporaryCredential: false }; - connection = await snowflake.createConnection(connectionOption); + authTest.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); + await connectAndProvideCredentials(connectToBrowserPromise); + authTest.verifyErrorWasThrown('Error while getting SAML token: Browser action timed out after 100 ms.'); + await authTest.verifyConnectionIsNotUp(); }); }); @@ -75,11 +73,11 @@ describe('External browser authentication tests', function () { }); it('obtains the id token from the server and saves it on the local storage', async function () { - connection = snowflake.createConnection(connectionOption); + authTest.createConnection(connectionOption); const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); - await connectAndProvideCredentials(connection, provideCredentialsPromise); - verifyNoErrorWasThrown(); - await verifyConnectionIsUp(connection); + await connectAndProvideCredentials(provideCredentialsPromise); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); }); it('the token is saved in the credential manager', async function () { @@ -88,19 +86,19 @@ describe('External browser authentication tests', function () { }); it('authenticates by token, browser credentials not needed', async function () { - connection = snowflake.createConnection(connectionOption); - await connection.connectAsync(connectAsyncCallback()); - verifyNoErrorWasThrown(); - await verifyConnectionIsUp(connection); + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); }); it('opens browser okta authentication again when token is incorrect', async function () { await defaultCredentialManager.write(key, '1234'); - connection = snowflake.createConnection(connectionOption); + authTest.createConnection(connectionOption); const provideCredentialsPromise = execWithTimeout('node', [provideBrowserCredentialsPath, 'success', login, password], 15000); - await connectAndProvideCredentials(connection, provideCredentialsPromise); - verifyNoErrorWasThrown(); - await verifyConnectionIsUp(connection); + await connectAndProvideCredentials(provideCredentialsPromise); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); }); it('refreshes the token for credential cache key', async function () { @@ -109,65 +107,21 @@ describe('External browser authentication tests', function () { }); }); - 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') { + if (!runAuthTestsManually) { await execWithTimeout('node', [cleanBrowserProcessesPath], 15000); } } - async function connectAndProvideCredentials(connection, provideCredentialsPromise) { - if (process.env.RUN_AUTH_TESTS_MANUALLY === 'true') { - await connection.connectAsync(connectAsyncCallback()); + async function connectAndProvideCredentials(provideCredentialsPromise) { + if (runAuthTestsManually) { + await authTest.connectAsync(); } 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; - } + await Promise.allSettled([authTest.connectAsync(), provideCredentialsPromise]); } - 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 }); diff --git a/test/authentication/testKeyPair.js b/test/authentication/testKeyPair.js new file mode 100644 index 000000000..60b48df28 --- /dev/null +++ b/test/authentication/testKeyPair.js @@ -0,0 +1,94 @@ +const AuthTest = require('./authTestsBaseClass'); +const connParameters = require('./connectionParameters'); +const snowflake = require('../../lib/snowflake'); +const path = require('path'); +const assert = require('node:assert'); +const fs = require('fs').promises; + + +describe('Key-pair authentication', function () { + let authTest; + + beforeEach(async () => { + authTest = new AuthTest(); + }); + + afterEach(async () => { + await authTest.destroyConnection(); + }); + + describe('Private key', function () { + it('Successful connection', async function () { + const privateKey = await getFileContent(connParameters.snowflakeAuthTestPrivateKeyPath); + const connectionOption = { ... connParameters.keypairPrivateKey, privateKey: privateKey }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); + }); + + it('Invalid private key format', async function () { + const invalidPrivateKeyFormat = 'invalidKey'; + const connectionOption = { ... connParameters.keypairPrivateKey, privateKey: invalidPrivateKeyFormat }; + try { + snowflake.createConnection(connectionOption); + assert.fail('Expected error was not thrown'); + } catch (err) { + assert.strictEqual(err.message, 'Invalid private key. The specified value must be a string in pem format of type pkcs8'); + } + }); + + it('Invalid private key', async function () { + const privateKey = await getFileContent(connParameters.snowflakeAuthTestInvalidPrivateKeyPath); + const connectionOption = { ... connParameters.keypairPrivateKey, privateKey: privateKey }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + assert.match(authTest.error?.message, /JWT token is invalid./); + await authTest.verifyConnectionIsNotUp('Unable to perform operation using terminated connection.'); + }); + }); + + describe('Private key path', function () { + it('Successful connection', async function () { + const connectionOption = connParameters.keypairPrivateKeyPath; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); + }); + + it('Invalid private key', async function () { + const connectionOption = { ...connParameters.keypairPrivateKeyPath, privateKeyPath: connParameters.snowflakeAuthTestInvalidPrivateKeyPath }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + assert.match(authTest.error?.message, /JWT token is invalid./); + await authTest.verifyConnectionIsNotUp('Unable to perform operation using terminated connection.'); + }); + + it('Successful connection using encrypted private key', async function () { + const connectionOption = connParameters.keypairEncryptedPrivateKeyPath; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); + }); + + //todo SNOW-1844747 improve error message + it('Invalid private key password', async function () { + const connectionOption = { ...connParameters.keypairEncryptedPrivateKeyPath, privateKeyPass: 'invalid' }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + assert.match(authTest.error?.message, /bad decrypt/); + await authTest.verifyConnectionIsNotUp(); + }); + }); +}); + +async function getFileContent(filePath) { + try { + const absolutePath = path.resolve(filePath); + return await fs.readFile(absolutePath, 'utf8'); + } catch (err) { + throw new Error(`Error reading file: ${err.message}`); + } +} diff --git a/test/authentication/testOauth.js b/test/authentication/testOauth.js new file mode 100644 index 000000000..5f1148626 --- /dev/null +++ b/test/authentication/testOauth.js @@ -0,0 +1,67 @@ +const assert = require('assert'); +const connParameters = require('./connectionParameters'); +const axios = require('axios'); +const { snowflakeAuthTestOktaUser, snowflakeAuthTestOktaPass, snowflakeAuthTestRole, snowflakeAuthTestOauthClientId, + snowflakeAuthTestOauthClientSecret, snowflakeAuthTestOauthUrl +} = require('./connectionParameters'); +const AuthTest = require('./authTestsBaseClass'); + + +describe('Oauth authentication', function () { + let authTest; + + beforeEach(async () => { + authTest = new AuthTest(); + }); + + afterEach(async () => { + await authTest.destroyConnection(); + }); + + it('Successful connection', async function () { + const token = await getToken(); + const connectionOption = { ...connParameters.oauth, token: token }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); + }); + + it('Invalid token', async function () { + const connectionOption = { ...connParameters.oauth, token: 'invalidToken' }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyErrorWasThrown('Invalid OAuth access token. '); + await authTest.verifyConnectionIsNotUp('Unable to perform operation using terminated connection.'); + }); + + it('Mismatched username', async function () { + const token = await getToken(); + const connectionOption = { ...connParameters.oauth, username: 'itsnotanaccount.com', token: token }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyErrorWasThrown('The user you were trying to authenticate as differs from the user tied to the access token.'); + await authTest.verifyConnectionIsNotUp('Unable to perform operation using terminated connection.'); + }); +}); + +async function getToken() { + const response = await axios.post(snowflakeAuthTestOauthUrl, data, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, + auth: { + username: snowflakeAuthTestOauthClientId, + password: snowflakeAuthTestOauthClientSecret + } + }); + assert.strictEqual(response.status, 200, 'Failed to get access token'); + return response.data.access_token; +} + +const data = [ + `username=${snowflakeAuthTestOktaUser}`, + `password=${snowflakeAuthTestOktaPass}`, + 'grant_type=password', + `scope=session:role:${snowflakeAuthTestRole.toLowerCase()}` +].join('&'); diff --git a/test/authentication/testOkta.js b/test/authentication/testOkta.js new file mode 100644 index 000000000..1230bb0db --- /dev/null +++ b/test/authentication/testOkta.js @@ -0,0 +1,39 @@ +const connParameters = require('./connectionParameters'); +const AuthTest = require('./authTestsBaseClass'); + +describe('Okta authentication', function () { + let authTest; + + beforeEach(async () => { + authTest = new AuthTest(); + }); + + afterEach(async () => { + await authTest.destroyConnection(); + }); + + it('Successful connection', async function () { + const connectionOption = connParameters.okta; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyNoErrorWasThrown(); + await authTest.verifyConnectionIsUp(); + }); + + it('Wrong credentials', async function () { + const connectionOption = { ...connParameters.okta, username: 'itsnotanaccount.com', password: 'fakepassword' }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyErrorWasThrown('Request failed with status code 401'); + await authTest.verifyConnectionIsNotUp(); + }); + + //todo SNOW-1844747 improve error message + it('Wrong okta url', async function () { + const connectionOption = { ...connParameters.okta, authenticator: 'https://testinvalidaccoount.com' }; + authTest.createConnection(connectionOption); + await authTest.connectAsync(); + authTest.verifyErrorWasThrown('Cannot read properties of null (reading \'ssoUrl\')'); + await authTest.verifyConnectionIsNotUp(); + }); +}); diff --git a/test/integration/connectionOptions.js b/test/integration/connectionOptions.js index 28185ff2e..88c56e344 100644 --- a/test/integration/connectionOptions.js +++ b/test/integration/connectionOptions.js @@ -16,16 +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 snowflakeTestPrivateKeyUser = process.env.SNOWFLAKE_JWT_TEST_USER; -const snowflakeTestPrivateKey = process.env.SNOWFLAKE_TEST_PRIVATE_KEY; -const snowflakeTestPrivateKeyPath = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_PATH; -const snowflakeTestPrivateKeyPass = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_PASS; -const snowflakeTestPrivateKeyPathUnencrypted = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_PATH_UNENCRYPTED; -const snowflakeTestOauthUser = process.env.SNOWFLAKE_TEST_OAUTH_USER; -const snowflakeTestToken = process.env.SNOWFLAKE_TEST_OAUTH_TOKEN; -const snowflakeTestOktaUser = process.env.SNOWFLAKE_TEST_OKTA_USER; -const snowflakeTestOktaPass = process.env.SNOWFLAKE_TEST_OKTA_PASS; -const snowflakeTestOktaAuth = process.env.SNOWFLAKE_TEST_OKTA_AUTH; const snowflakeTestPasscode = process.env.SNOWFLAKE_TEST_PASSCODE; if (snowflakeTestProtocol === undefined) { @@ -88,96 +78,12 @@ const wrongPwd = account: snowflakeTestAccount }; -const keypairPrivateKey = -{ - accessUrl: accessUrl, - username: snowflakeTestPrivateKeyUser, - account: snowflakeTestAccount, - warehouse: snowflakeTestWarehouse, - database: snowflakeTestDatabase, - schema: snowflakeTestSchema, - role: snowflakeTestRole, - privateKey: snowflakeTestPrivateKey, - authenticator: 'SNOWFLAKE_JWT' -}; - -const keypairPathEncrypted = -{ - accessUrl: accessUrl, - username: snowflakeTestPrivateKeyUser, - account: snowflakeTestAccount, - warehouse: snowflakeTestWarehouse, - database: snowflakeTestDatabase, - schema: snowflakeTestSchema, - role: snowflakeTestRole, - privateKeyPath: snowflakeTestPrivateKeyPath, - privateKeyPass: snowflakeTestPrivateKeyPass, - authenticator: 'SNOWFLAKE_JWT' -}; - -const keypairPathUnencrypted = -{ - accessUrl: accessUrl, - username: snowflakeTestPrivateKeyUser, - account: snowflakeTestAccount, - warehouse: snowflakeTestWarehouse, - database: snowflakeTestDatabase, - schema: snowflakeTestSchema, - role: snowflakeTestRole, - privateKeyPath: snowflakeTestPrivateKeyPathUnencrypted, - authenticator: 'SNOWFLAKE_JWT' -}; - -const keypairWrongToken = -{ - accessUrl: accessUrl, - username: 'node', - account: snowflakeTestAccount, - privateKey: snowflakeTestPrivateKey, - authenticator: 'SNOWFLAKE_JWT' -}; - const MFA = { ...valid, authenticator: 'USER_PWD_MFA_AUTHENTICATOR', passcode: snowflakeTestPasscode, }; -const oauth = -{ - accessUrl: accessUrl, - username: snowflakeTestOauthUser, - account: snowflakeTestAccount, - warehouse: snowflakeTestWarehouse, - database: snowflakeTestDatabase, - schema: snowflakeTestSchema, - role: snowflakeTestRole, - token: snowflakeTestToken, - authenticator: 'OAUTH' -}; - -const oauthMismatchUser = -{ - accessUrl: accessUrl, - username: 'node', - account: snowflakeTestAccount, - token: snowflakeTestToken, - authenticator: 'OAUTH' -}; - -const okta = -{ - accessUrl: accessUrl, - username: snowflakeTestOktaUser, - password: snowflakeTestOktaPass, - account: snowflakeTestAccount, - warehouse: snowflakeTestWarehouse, - database: snowflakeTestDatabase, - schema: snowflakeTestSchema, - role: snowflakeTestRole, - authenticator: snowflakeTestOktaAuth -}; - const privatelink = { accessUrl: accessUrl, @@ -206,13 +112,6 @@ exports.wrongUserName = wrongUserName; exports.wrongPwd = wrongPwd; exports.accessUrl = accessUrl; exports.account = snowflakeTestAccount; -exports.keypairPrivateKey = keypairPrivateKey; -exports.keypairPathEncrypted = keypairPathEncrypted; -exports.keypairPathUnencrypted = keypairPathUnencrypted; -exports.keypairWrongToken = keypairWrongToken; -exports.oauth = oauth; -exports.oauthMismatchUser = oauthMismatchUser; -exports.okta = okta; exports.privatelink = privatelink; exports.connectionWithProxy = connectionWithProxy; exports.MFA = MFA; diff --git a/test/integration/testManualConnection.js b/test/integration/testManualConnection.js index 57f148412..9fe3ad78d 100644 --- a/test/integration/testManualConnection.js +++ b/test/integration/testManualConnection.js @@ -3,7 +3,6 @@ */ const snowflake = require('./../../lib/snowflake'); -const async = require('async'); const assert = require('assert'); const connOption = require('./connectionOptions'); const testUtil = require('./testUtil'); @@ -13,87 +12,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 - oauth', function () { - it('Simple Connect', function (done) { - const connection = snowflake.createConnection(connOption.oauth); - - async.series([ - function (callback) { - connection.connect(function (err) { - done(err); - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(connection.isUp(), 'not active'); - callback(); - }, - function (callback) { - connection.destroy(function (err) { - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(!connection.isUp(), 'still active'); - callback(); - }, - ]); - }); - - it('Mismatched Username', function (done) { - const connection = snowflake.createConnection( - connOption.oauthMismatchUser - ); - connection.connect(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 tied to the access token.', - err['message'] - ); - done(); - } catch (err) { - done(err); - } - }); - }); - }); - - describe('Connection test - okta', function () { - it('Simple Connect', function (done) { - const connection = snowflake.createConnection(connOption.okta); - - async.series([ - function (callback) { - connection.connectAsync(function (err) { - done(err); - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(connection.isUp(), 'not active'); - callback(); - }, - function (callback) { - connection.destroy(function (err) { - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(!connection.isUp(), 'still active'); - callback(); - }, - ]); - }); - }); - describe('Connection - MFA authenticator with DUO', function () { const connectionOption = connOption.MFA; @@ -182,113 +100,6 @@ if (process.env.RUN_MANUAL_TESTS_ONLY === 'true') { }); }); }); - - describe('Connection test - keypair', function () { - it('Simple Connect - specify private key', function (done) { - const connection = snowflake.createConnection( - connOption.keypairPrivateKey - ); - - async.series([ - function (callback) { - connection.connect(function (err) { - done(err); - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(connection.isUp(), 'not active'); - callback(); - }, - function (callback) { - connection.destroy(function (err) { - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(!connection.isUp(), 'still active'); - callback(); - }, - ]); - }); - - it('Simple Connect - specify encrypted private key path and passphrase', function (done) { - const connection = snowflake.createConnection( - connOption.keypairPathEncrypted - ); - - async.series([ - function (callback) { - connection.connect(function (err) { - done(err); - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(connection.isUp(), 'not active'); - callback(); - }, - function (callback) { - connection.destroy(function (err) { - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(!connection.isUp(), 'still active'); - callback(); - }, - ]); - }); - - it('Simple Connect - specify unencrypted private key path without passphrase', function (done) { - const connection = snowflake.createConnection( - connOption.keypairPathEncrypted - ); - - async.series([ - function (callback) { - connection.connect(function (err) { - done(err); - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(connection.isUp(), 'not active'); - callback(); - }, - function (callback) { - connection.destroy(function (err) { - assert.ok(!err, JSON.stringify(err)); - callback(); - }); - }, - function (callback) { - assert.ok(!connection.isUp(), 'still active'); - callback(); - }, - ]); - }); - - it('Wrong JWT token', function (done) { - const connection = snowflake.createConnection( - connOption.keypairWrongToken - ); - connection.connect(function (err) { - try { - assert.ok(err, 'Incorrect JWT token is passed.'); - assert.equal('JWT token is invalid.', err['message']); - done(); - } catch (err) { - done(err); - } - }); - }); - }); }); describe('keepAlive test', function () {