Skip to content

Commit e4b0340

Browse files
SNOW-1883649 wiremock integration and mappings for oauth
1 parent ea8a0a1 commit e4b0340

24 files changed

+1562
-12
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved.
3+
*/
4+
5+
const Util = require('../util');
6+
/**
7+
* Creates an oauth authenticator.
8+
*
9+
* @param {String} token
10+
*
11+
* @returns {Object}
12+
* @constructor
13+
*/
14+
function AuthOauthPAT(token, password) {
15+
/**
16+
* Update JSON body with token.
17+
*
18+
* @param {JSON} body
19+
*
20+
* @returns {null}
21+
*/
22+
this.updateBody = function (body) {
23+
if (Util.exists(token)) {
24+
body['data']['TOKEN'] = token;
25+
} else if (Util.exists(password)) {
26+
body['data']['TOKEN'] = password;
27+
}
28+
};
29+
30+
this.authenticate = async function () {};
31+
}
32+
module.exports = AuthOauthPAT;

lib/authentication/authentication.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const AuthDefault = require('./auth_default');
66
const AuthWeb = require('./auth_web');
77
const AuthKeypair = require('./auth_keypair');
88
const AuthOauth = require('./auth_oauth');
9+
const AuthOauthPAT = require('./auth_oauth_pat');
910
const AuthOkta = require('./auth_okta');
1011
const AuthIDToken = require('./auth_idtoken');
1112
const Logger = require('../logger');
@@ -73,8 +74,10 @@ exports.getAuthenticator = function getAuthenticator(connectionConfig, httpClien
7374
}
7475
} else if (authType === AuthenticationTypes.KEY_PAIR_AUTHENTICATOR) {
7576
auth = new AuthKeypair(connectionConfig);
76-
} else if (authType === AuthenticationTypes.OAUTH_AUTHENTICATOR) {
77+
} else if (authType === AuthenticationTypes.OAUTH_AUTHENTICATOR ) {
7778
auth = new AuthOauth(connectionConfig.getToken());
79+
} else if (authType === AuthenticationTypes.PROGRAMMATIC_ACCESS_TOKEN ) {
80+
auth = new AuthOauthPAT(connectionConfig.getToken(), connectionConfig.password);
7881
} else if (this.isOktaAuth(authType)) {
7982
auth = new AuthOkta(connectionConfig, httpClient);
8083
} else {

lib/authentication/authentication_types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const AuthenticationTypes =
66
OAUTH_AUTHENTICATOR: 'OAUTH',
77
USER_PWD_MFA_AUTHENTICATOR: 'USERNAME_PASSWORD_MFA',
88
ID_TOKEN_AUTHENTICATOR: 'ID_TOKEN',
9+
PROGRAMMATIC_ACCESS_TOKEN: 'PROGRAMMATIC_ACCESS_TOKEN',
910
};
1011

1112
module.exports = AuthenticationTypes;

lib/connection/connection_config.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,8 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
170170
// username is not required for oauth and external browser authenticators
171171
if (!Util.exists(options.authenticator) ||
172172
(options.authenticator.toUpperCase() !== AuthenticationTypes.OAUTH_AUTHENTICATOR &&
173-
options.authenticator.toUpperCase() !== AuthenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR)) {
173+
options.authenticator.toUpperCase() !== AuthenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR &&
174+
options.authenticator.toUpperCase() !== AuthenticationTypes.PROGRAMMATIC_ACCESS_TOKEN)) {
174175
// check for missing username
175176
Errors.checkArgumentExists(Util.exists(options.username),
176177
ErrorCodes.ERR_CONN_CREATE_MISSING_USERNAME);
@@ -194,6 +195,23 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
194195
ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD);
195196
}
196197

198+
if (!Util.exists(options.authenticator) ||
199+
options.authenticator === AuthenticationTypes.PROGRAMMATIC_ACCESS_TOKEN) {
200+
// PASSWORD or TOKEN is needed
201+
Errors.checkArgumentExists(Util.exists(options.password) || Util.exists(options.token),
202+
ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD);
203+
204+
if (Util.exists(options.password)) {
205+
// check for invalid password
206+
Errors.checkArgumentValid(Util.isString(options.password),
207+
ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD);
208+
}
209+
if (Util.exists(options.token)) {
210+
Errors.checkArgumentValid(Util.isString(options.token),
211+
ErrorCodes.ERR_CONN_CREATE_INVALID_OAUTH_TOKEN);
212+
}
213+
}
214+
197215
consolidateHostAndAccount(options);
198216
}
199217

lib/constants/error_messages.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ exports[404053] = 'A host must be specified.';
8484
exports[404054] = 'Invalid host. The specified value must be a string.';
8585
exports[404055] = 'Invalid passcodeInPassword. The specified value must be a boolean';
8686
exports[404056] = 'Invalid passcode. The specified value must be a string';
87+
exports[404057] = 'A password or token must be specified.';
8788

8889
// 405001
8990
exports[405001] = 'Invalid callback. The specified value must be a function.';

lib/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ codes.ERR_CONN_CREATE_MISSING_HOST = 404053;
8888
codes.ERR_CONN_CREATE_INVALID_HOST = 404054;
8989
codes.ERR_CONN_CREATE_INVALID_PASSCODE_IN_PASSWORD = 404055;
9090
codes.ERR_CONN_CREATE_INVALID_PASSCODE = 404056;
91+
codes.ERR_CONN_CREATE_MISSING_PASSWORD_AND_TOKEN = 404057;
9192

9293
// 405001
9394
codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001;

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
"mocha": "^10.2.0",
4343
"mock-require": "^3.0.3",
4444
"nyc": "^15.1.0",
45-
"test-console": "^2.0.0"
45+
"test-console": "^2.0.0",
46+
"wiremock": "^3.10.0",
47+
"wiremock-rest-client": "^1.11.0"
4648
},
4749
"peerDependencies": {
4850
"asn1.js": "^5.4.1"
@@ -71,7 +73,9 @@
7173
"test:ci:coverage": "nyc npm run test:ci",
7274
"test:ci:withSystemTests": "mocha -timeout 180000 --recursive --full-trace 'test/{unit,integration}/**/*.js' system_test/*.js",
7375
"test:ci:withSystemTests:coverage": "nyc npm run test:ci:withSystemTests",
74-
"test:manual": "mocha -timeout 180000 --full-trace --full-trace test/integration/testManualConnection.js"
76+
"test:manual": "mocha -timeout 180000 --full-trace --full-trace test/integration/testManualConnection.js",
77+
"serve-wiremock": "wiremock --enable-browser-proxying --proxy-pass-through false --port 8081 ",
78+
"wiremock": "npm run serve-wiremock"
7579
},
7680
"author": {
7781
"name": "Snowflake Computing, Inc.",

test/authentication/connectionParameters.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ const oauth =
5454
authenticator: 'OAUTH'
5555
};
5656

57+
const oauthPATOnWiremock =
58+
{
59+
...baseParameters,
60+
accessUrl: null,
61+
username: 'MOCK_USERNAME',
62+
account: 'MOCK_ACCOUNT_NAME',
63+
host: 'localhost',
64+
protocol: 'http',
65+
authenticator: 'PROGRAMMATIC_ACCESS_TOKEN',
66+
// proxyHost: '127.0.0.1',
67+
// proxyPort: 8080
68+
};
69+
5770
const keypairPrivateKey =
5871
{
5972
...baseParameters,
@@ -81,6 +94,7 @@ const keypairEncryptedPrivateKeyPath =
8194
exports.externalBrowser = externalBrowser;
8295
exports.okta = okta;
8396
exports.oauth = oauth;
97+
exports.oauthPATOnWiremock = oauthPATOnWiremock;
8498
exports.keypairPrivateKey = keypairPrivateKey;
8599
exports.keypairPrivateKeyPath = keypairPrivateKeyPath;
86100
exports.keypairEncryptedPrivateKeyPath = keypairEncryptedPrivateKeyPath;

test/authentication/testOauth.js

Lines changed: 136 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,86 @@
11
const assert = require('assert');
2+
const fs = require('fs');
3+
const net = require('net');
24
const connParameters = require('./connectionParameters');
35
const axios = require('axios');
4-
const { snowflakeAuthTestOktaUser, snowflakeAuthTestOktaPass, snowflakeAuthTestRole, snowflakeAuthTestOauthClientId,
6+
const { snowflakeAuthTestOauthClientId,
57
snowflakeAuthTestOauthClientSecret, snowflakeAuthTestOauthUrl
68
} = require('./connectionParameters');
79
const AuthTest = require('./authTestsBaseClass');
10+
const WireMockRestClient = require('wiremock-rest-client').WireMockRestClient;
11+
const { exec } = require('child_process');
812

913

14+
15+
async function runWireMockAsync(port) {
16+
let timeoutHandle;
17+
const waitingWireMockPromise = new Promise(async (resolve, reject) => {
18+
try {
19+
exec(`npx wiremock --enable-browser-proxying --proxy-pass-through false --port ${port} `);
20+
const wireMock = new WireMockRestClient(`http://localhost:${port}`);
21+
const readyWireMock = await waitForWiremockStarted(wireMock);
22+
resolve(readyWireMock);
23+
} catch (err) {
24+
reject(err);
25+
}
26+
});
27+
28+
29+
const timeout = new Promise((resolve, reject) =>
30+
timeoutHandle = setTimeout(
31+
() => reject('Wiremock unavailable after 6000 ms.'),
32+
6000));
33+
return Promise.race([waitingWireMockPromise, timeout])
34+
.then(result => {
35+
clearTimeout(timeoutHandle);
36+
return result;
37+
});
38+
}
39+
40+
async function waitForWiremockStarted(wireMock) {
41+
return fetch(wireMock.baseUri)
42+
.then(async (resp) => {
43+
if (resp.ok) {
44+
return Promise.resolve(wireMock);
45+
} else {
46+
await new Promise(resolve => setTimeout(resolve, 1000));
47+
console.log(`Retry connection to WireMock after wrong response status: ${resp.status}`);
48+
return await waitForWiremockStarted(wireMock);
49+
}
50+
})
51+
.catch(async (err) => {
52+
await new Promise(resolve => setTimeout(resolve, 1000));
53+
console.log(`Retry connection to WireMock after error: ${err}`);
54+
return await waitForWiremockStarted(wireMock);
55+
});
56+
}
57+
58+
describe('Wiremock test', function () {
59+
it('Run Wiremock instance, wait, verify connection and shutdown', async function () {
60+
const wireMock = await runWireMockAsync();
61+
try {
62+
assert.doesNotReject(async () => await wireMock.mappings.getAllMappings());
63+
} finally {
64+
await wireMock.global.shutdown();
65+
}
66+
});
67+
it('Add mappings', async function () {
68+
const wireMock = await runWireMockAsync();
69+
try {
70+
const requests = JSON.parse(fs.readFileSync('wiremock/mappings/test.json', 'utf8'));
71+
for (const mapping of requests.mappings) {
72+
await wireMock.mappings.createMapping(mapping);
73+
}
74+
const mappings = await wireMock.mappings.getAllMappings();
75+
assert.strictEqual(mappings.mappings.length, 2);
76+
const response = await axios.get('http://localhost:8081/test/authorize.html');
77+
assert.strictEqual(response.status, 200);
78+
} finally {
79+
await wireMock.global.shutdown();
80+
}
81+
});
82+
});
83+
1084
describe('Oauth authentication', function () {
1185
let authTest;
1286

@@ -45,8 +119,59 @@ describe('Oauth authentication', function () {
45119
});
46120
});
47121

122+
describe('Oauth PAT authentication', function () {
123+
let port;
124+
let authTest;
125+
let wireMock;
126+
before(async () => {
127+
port = await getPortFree();
128+
wireMock = await runWireMockAsync(port);
129+
});
130+
beforeEach(async () => {
131+
authTest = new AuthTest();
132+
});
133+
afterEach(async () => {
134+
wireMock.scenarios.resetAllScenarios();
135+
});
136+
after(async () => {
137+
await wireMock.global.shutdown();
138+
});
139+
140+
141+
it('Successful flow scenario PAT as token', async function () {
142+
await addWireMockMappingsFromFile('wiremock/mappings/pat/successful_flow.json');
143+
const connectionOption = { ...connParameters.oauthPATOnWiremock, token: 'MOCK_TOKEN', port: port };
144+
authTest.createConnection(connectionOption);
145+
await authTest.connectAsync();
146+
authTest.verifyNoErrorWasThrown();
147+
});
148+
149+
it('Successful flow scenario PAT as password', async function () {
150+
await addWireMockMappingsFromFile('wiremock/mappings/pat/successful_flow.json');
151+
const connectionOption = { ...connParameters.oauthPATOnWiremock, password: 'MOCK_TOKEN', port: port };
152+
authTest.createConnection(connectionOption);
153+
await authTest.connectAsync();
154+
authTest.verifyNoErrorWasThrown();
155+
});
156+
157+
it('Invalid token', async function () {
158+
await addWireMockMappingsFromFile('wiremock/mappings/pat/invalid_pat_token.json');
159+
const connectionOption = { ...connParameters.oauthPATOnWiremock, token: 'INVALID_TOKEN', port: port };
160+
authTest.createConnection(connectionOption);
161+
await authTest.connectAsync();
162+
authTest.verifyErrorWasThrown('Programmatic access token is invalid.');
163+
});
164+
165+
async function addWireMockMappingsFromFile(filePath) {
166+
const requests = JSON.parse(fs.readFileSync(filePath, 'utf8'));
167+
for (const mapping of requests.mappings) {
168+
await wireMock.mappings.createMapping(mapping);
169+
}
170+
}
171+
});
172+
48173
async function getToken() {
49-
const response = await axios.post(snowflakeAuthTestOauthUrl, data, {
174+
const response = await axios.post(snowflakeAuthTestOauthUrl, {}, {
50175
headers: {
51176
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
52177
},
@@ -59,9 +184,12 @@ async function getToken() {
59184
return response.data.access_token;
60185
}
61186

62-
const data = [
63-
`username=${snowflakeAuthTestOktaUser}`,
64-
`password=${snowflakeAuthTestOktaPass}`,
65-
'grant_type=password',
66-
`scope=session:role:${snowflakeAuthTestRole.toLowerCase()}`
67-
].join('&');
187+
async function getPortFree() {
188+
return new Promise( res => {
189+
const srv = net.createServer();
190+
srv.listen(0, () => {
191+
const port = srv.address().port;
192+
srv.close((err) => res(port));
193+
});
194+
});
195+
}

0 commit comments

Comments
 (0)