Skip to content

Commit

Permalink
SNOW-1883649 wiremock integration and mappings for oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-pmotacki committed Jan 22, 2025
1 parent ea8a0a1 commit e4b0340
Show file tree
Hide file tree
Showing 24 changed files with 1,562 additions and 12 deletions.
32 changes: 32 additions & 0 deletions lib/authentication/auth_oauth_pat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved.
*/

const Util = require('../util');
/**
* Creates an oauth authenticator.
*
* @param {String} token
*
* @returns {Object}
* @constructor
*/
function AuthOauthPAT(token, password) {
/**
* Update JSON body with token.
*
* @param {JSON} body
*
* @returns {null}
*/
this.updateBody = function (body) {
if (Util.exists(token)) {
body['data']['TOKEN'] = token;
} else if (Util.exists(password)) {
body['data']['TOKEN'] = password;
}
};

this.authenticate = async function () {};
}
module.exports = AuthOauthPAT;
5 changes: 4 additions & 1 deletion lib/authentication/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const AuthDefault = require('./auth_default');
const AuthWeb = require('./auth_web');
const AuthKeypair = require('./auth_keypair');
const AuthOauth = require('./auth_oauth');
const AuthOauthPAT = require('./auth_oauth_pat');
const AuthOkta = require('./auth_okta');
const AuthIDToken = require('./auth_idtoken');
const Logger = require('../logger');
Expand Down Expand Up @@ -73,8 +74,10 @@ exports.getAuthenticator = function getAuthenticator(connectionConfig, httpClien
}
} else if (authType === AuthenticationTypes.KEY_PAIR_AUTHENTICATOR) {
auth = new AuthKeypair(connectionConfig);
} else if (authType === AuthenticationTypes.OAUTH_AUTHENTICATOR) {
} else if (authType === AuthenticationTypes.OAUTH_AUTHENTICATOR ) {
auth = new AuthOauth(connectionConfig.getToken());
} else if (authType === AuthenticationTypes.PROGRAMMATIC_ACCESS_TOKEN ) {
auth = new AuthOauthPAT(connectionConfig.getToken(), connectionConfig.password);
} else if (this.isOktaAuth(authType)) {
auth = new AuthOkta(connectionConfig, httpClient);
} else {
Expand Down
1 change: 1 addition & 0 deletions lib/authentication/authentication_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const AuthenticationTypes =
OAUTH_AUTHENTICATOR: 'OAUTH',
USER_PWD_MFA_AUTHENTICATOR: 'USERNAME_PASSWORD_MFA',
ID_TOKEN_AUTHENTICATOR: 'ID_TOKEN',
PROGRAMMATIC_ACCESS_TOKEN: 'PROGRAMMATIC_ACCESS_TOKEN',
};

module.exports = AuthenticationTypes;
20 changes: 19 additions & 1 deletion lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
// username is not required for oauth and external browser authenticators
if (!Util.exists(options.authenticator) ||
(options.authenticator.toUpperCase() !== AuthenticationTypes.OAUTH_AUTHENTICATOR &&
options.authenticator.toUpperCase() !== AuthenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR)) {
options.authenticator.toUpperCase() !== AuthenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR &&
options.authenticator.toUpperCase() !== AuthenticationTypes.PROGRAMMATIC_ACCESS_TOKEN)) {
// check for missing username
Errors.checkArgumentExists(Util.exists(options.username),
ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME);
Expand All @@ -194,6 +195,23 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD);
}

if (!Util.exists(options.authenticator) ||
options.authenticator === AuthenticationTypes.PROGRAMMATIC_ACCESS_TOKEN) {
// PASSWORD or TOKEN is needed
Errors.checkArgumentExists(Util.exists(options.password) || Util.exists(options.token),
ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD);

if (Util.exists(options.password)) {
// check for invalid password
Errors.checkArgumentValid(Util.isString(options.password),
ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD);
}
if (Util.exists(options.token)) {
Errors.checkArgumentValid(Util.isString(options.token),
ErrorCodes.ERR_CONN_CREATE_INVALID_OAUTH_TOKEN);
}
}

consolidateHostAndAccount(options);
}

Expand Down
1 change: 1 addition & 0 deletions lib/constants/error_messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ exports[404053] = 'A host must be specified.';
exports[404054] = 'Invalid host. The specified value must be a string.';
exports[404055] = 'Invalid passcodeInPassword. The specified value must be a boolean';
exports[404056] = 'Invalid passcode. The specified value must be a string';
exports[404057] = 'A password or token must be specified.';

// 405001
exports[405001] = 'Invalid callback. The specified value must be a function.';
Expand Down
1 change: 1 addition & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ codes.ERR_CONN_CREATE_MISSING_HOST = 404053;
codes.ERR_CONN_CREATE_INVALID_HOST = 404054;
codes.ERR_CONN_CREATE_INVALID_PASSCODE_IN_PASSWORD = 404055;
codes.ERR_CONN_CREATE_INVALID_PASSCODE = 404056;
codes.ERR_CONN_CREATE_MISSING_PASSWORD_AND_TOKEN = 404057;

// 405001
codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001;
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
"mocha": "^10.2.0",
"mock-require": "^3.0.3",
"nyc": "^15.1.0",
"test-console": "^2.0.0"
"test-console": "^2.0.0",
"wiremock": "^3.10.0",
"wiremock-rest-client": "^1.11.0"
},
"peerDependencies": {
"asn1.js": "^5.4.1"
Expand Down Expand Up @@ -71,7 +73,9 @@
"test:ci:coverage": "nyc npm run test:ci",
"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"
"test:manual": "mocha -timeout 180000 --full-trace --full-trace test/integration/testManualConnection.js",
"serve-wiremock": "wiremock --enable-browser-proxying --proxy-pass-through false --port 8081 ",
"wiremock": "npm run serve-wiremock"
},
"author": {
"name": "Snowflake Computing, Inc.",
Expand Down
14 changes: 14 additions & 0 deletions test/authentication/connectionParameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,19 @@ const oauth =
authenticator: 'OAUTH'
};

const oauthPATOnWiremock =
{
...baseParameters,
accessUrl: null,
username: 'MOCK_USERNAME',
account: 'MOCK_ACCOUNT_NAME',
host: 'localhost',
protocol: 'http',
authenticator: 'PROGRAMMATIC_ACCESS_TOKEN',
// proxyHost: '127.0.0.1',
// proxyPort: 8080
};

const keypairPrivateKey =
{
...baseParameters,
Expand Down Expand Up @@ -81,6 +94,7 @@ const keypairEncryptedPrivateKeyPath =
exports.externalBrowser = externalBrowser;
exports.okta = okta;
exports.oauth = oauth;
exports.oauthPATOnWiremock = oauthPATOnWiremock;
exports.keypairPrivateKey = keypairPrivateKey;
exports.keypairPrivateKeyPath = keypairPrivateKeyPath;
exports.keypairEncryptedPrivateKeyPath = keypairEncryptedPrivateKeyPath;
Expand Down
144 changes: 136 additions & 8 deletions test/authentication/testOauth.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,86 @@
const assert = require('assert');
const fs = require('fs');
const net = require('net');
const connParameters = require('./connectionParameters');
const axios = require('axios');
const { snowflakeAuthTestOktaUser, snowflakeAuthTestOktaPass, snowflakeAuthTestRole, snowflakeAuthTestOauthClientId,
const { snowflakeAuthTestOauthClientId,
snowflakeAuthTestOauthClientSecret, snowflakeAuthTestOauthUrl
} = require('./connectionParameters');
const AuthTest = require('./authTestsBaseClass');
const WireMockRestClient = require('wiremock-rest-client').WireMockRestClient;
const { exec } = require('child_process');



async function runWireMockAsync(port) {
let timeoutHandle;
const waitingWireMockPromise = new Promise(async (resolve, reject) => {

Check failure on line 17 in test/authentication/testOauth.js

View workflow job for this annotation

GitHub Actions / Run lint

Promise executor functions should not be async
try {
exec(`npx wiremock --enable-browser-proxying --proxy-pass-through false --port ${port} `);
const wireMock = new WireMockRestClient(`http://localhost:${port}`);
const readyWireMock = await waitForWiremockStarted(wireMock);
resolve(readyWireMock);
} catch (err) {
reject(err);
}
});


const timeout = new Promise((resolve, reject) =>
timeoutHandle = setTimeout(
() => reject('Wiremock unavailable after 6000 ms.'),
6000));
return Promise.race([waitingWireMockPromise, timeout])
.then(result => {
clearTimeout(timeoutHandle);
return result;
});
}

async function waitForWiremockStarted(wireMock) {
return fetch(wireMock.baseUri)
.then(async (resp) => {
if (resp.ok) {
return Promise.resolve(wireMock);
} else {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Retry connection to WireMock after wrong response status: ${resp.status}`);

Check failure on line 47 in test/authentication/testOauth.js

View workflow job for this annotation

GitHub Actions / Run lint

Unexpected console statement
return await waitForWiremockStarted(wireMock);
}
})
.catch(async (err) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Retry connection to WireMock after error: ${err}`);

Check failure on line 53 in test/authentication/testOauth.js

View workflow job for this annotation

GitHub Actions / Run lint

Unexpected console statement
return await waitForWiremockStarted(wireMock);
});
}

describe('Wiremock test', function () {
it('Run Wiremock instance, wait, verify connection and shutdown', async function () {
const wireMock = await runWireMockAsync();
try {
assert.doesNotReject(async () => await wireMock.mappings.getAllMappings());
} finally {
await wireMock.global.shutdown();
}
});
it('Add mappings', async function () {
const wireMock = await runWireMockAsync();
try {
const requests = JSON.parse(fs.readFileSync('wiremock/mappings/test.json', 'utf8'));
for (const mapping of requests.mappings) {
await wireMock.mappings.createMapping(mapping);
}
const mappings = await wireMock.mappings.getAllMappings();
assert.strictEqual(mappings.mappings.length, 2);
const response = await axios.get('http://localhost:8081/test/authorize.html');
assert.strictEqual(response.status, 200);
} finally {
await wireMock.global.shutdown();
}
});
});

describe('Oauth authentication', function () {
let authTest;

Expand Down Expand Up @@ -45,8 +119,59 @@ describe('Oauth authentication', function () {
});
});

describe('Oauth PAT authentication', function () {
let port;
let authTest;
let wireMock;
before(async () => {
port = await getPortFree();
wireMock = await runWireMockAsync(port);
});
beforeEach(async () => {
authTest = new AuthTest();
});
afterEach(async () => {
wireMock.scenarios.resetAllScenarios();
});
after(async () => {
await wireMock.global.shutdown();
});


it('Successful flow scenario PAT as token', async function () {
await addWireMockMappingsFromFile('wiremock/mappings/pat/successful_flow.json');
const connectionOption = { ...connParameters.oauthPATOnWiremock, token: 'MOCK_TOKEN', port: port };
authTest.createConnection(connectionOption);
await authTest.connectAsync();
authTest.verifyNoErrorWasThrown();
});

it('Successful flow scenario PAT as password', async function () {
await addWireMockMappingsFromFile('wiremock/mappings/pat/successful_flow.json');
const connectionOption = { ...connParameters.oauthPATOnWiremock, password: 'MOCK_TOKEN', port: port };
authTest.createConnection(connectionOption);
await authTest.connectAsync();
authTest.verifyNoErrorWasThrown();
});

it('Invalid token', async function () {
await addWireMockMappingsFromFile('wiremock/mappings/pat/invalid_pat_token.json');
const connectionOption = { ...connParameters.oauthPATOnWiremock, token: 'INVALID_TOKEN', port: port };
authTest.createConnection(connectionOption);
await authTest.connectAsync();
authTest.verifyErrorWasThrown('Programmatic access token is invalid.');
});

async function addWireMockMappingsFromFile(filePath) {
const requests = JSON.parse(fs.readFileSync(filePath, 'utf8'));
for (const mapping of requests.mappings) {
await wireMock.mappings.createMapping(mapping);
}
}
});

async function getToken() {
const response = await axios.post(snowflakeAuthTestOauthUrl, data, {
const response = await axios.post(snowflakeAuthTestOauthUrl, {}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
Expand All @@ -59,9 +184,12 @@ async function getToken() {
return response.data.access_token;
}

const data = [
`username=${snowflakeAuthTestOktaUser}`,
`password=${snowflakeAuthTestOktaPass}`,
'grant_type=password',
`scope=session:role:${snowflakeAuthTestRole.toLowerCase()}`
].join('&');
async function getPortFree() {
return new Promise( res => {
const srv = net.createServer();
srv.listen(0, () => {
const port = srv.address().port;
srv.close((err) => res(port));

Check failure on line 192 in test/authentication/testOauth.js

View workflow job for this annotation

GitHub Actions / Run lint

'err' is defined but never used
});
});
}
Loading

0 comments on commit e4b0340

Please sign in to comment.