diff --git a/lib/authentication/auth_web.js b/lib/authentication/auth_web.js index f86a3cff1..3b822d5a4 100644 --- a/lib/authentication/auth_web.js +++ b/lib/authentication/auth_web.js @@ -3,8 +3,6 @@ */ const util = require('../util'); -const rest = require('../global_config').rest; - const net = require('net'); const querystring = require('querystring'); const URLUtil = require('./../../lib/url_util'); @@ -13,15 +11,17 @@ const Util = require('./../../lib/util'); /** * Creates an external browser authenticator. * - * @param {String} host + * @param {Object} connectionConfig + * @param {Object} ssoUrlProvider * @param {module} webbrowser - * @param {module} httpclient - * @param {module} browserActionTimeout * * @returns {Object} * @constructor */ -function auth_web(host, browserActionTimeout, webbrowser, httpclient, ) { +function auth_web(connectionConfig, ssoUrlProvider, webbrowser) { + + const host = connectionConfig.host; + const browserActionTimeout = connectionConfig.getBrowserActionTimeout(); if (!Util.exists(host)) { throw new Error(`Invalid value for host: ${host}`); @@ -31,14 +31,10 @@ function auth_web(host, browserActionTimeout, webbrowser, httpclient, ) { } const open = typeof webbrowser !== "undefined" ? webbrowser : require('open'); - const axios = typeof httpclient !== "undefined" ? httpclient : require('axios'); - const browserTimeout = browserActionTimeout - const port = rest.HTTPS_PORT; - const protocol = rest.HTTPS_PROTOCOL; + let ssoURL; let proofKey; let token; - let data; const successResponse = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nYour identity was confirmed and propagated to Snowflake Node.js driver. You can close this window now and go back where you started from.'; @@ -79,11 +75,14 @@ function auth_web(host, browserActionTimeout, webbrowser, httpclient, ) { server.listen(0, 0); // Step 1: query Snowflake to obtain SSO url - const ssoURL = await getSSOURL(authenticator, + const ssoData = await ssoUrlProvider.getSSOURL(authenticator, serviceName, account, server.address().port, - username); + username, + host); + ssoURL = ssoData['ssoUrl']; + proofKey = ssoData['proofKey']; // Step 2: validate URL if (!URLUtil.isValidURL(ssoURL)) { @@ -94,8 +93,8 @@ function auth_web(host, browserActionTimeout, webbrowser, httpclient, ) { open(ssoURL); // Step 4: get SAML token - data = await withBrowserActionTimeout(browserActionTimeout, receiveData) - processGet(data); + const tokenData = await withBrowserActionTimeout(browserActionTimeout, receiveData) + processGet(tokenData); }; /** @@ -139,60 +138,6 @@ function auth_web(host, browserActionTimeout, webbrowser, httpclient, ) { return server; }; - /** - * Get SSO URL through POST request. - * - * @param {String} authenticator - * @param {String} serviceName - * @param {String} account - * @param {Number} callback_port - * @param {String} user - * - * @returns {String} the SSO URL. - */ - function getSSOURL(authenticator, serviceName, account, callback_port, user) - { - // Create URL to send POST request to - const url = protocol + '://' + host + "/session/authenticator-request"; - - let header; - if (serviceName) - { - header = { - 'HTTP_HEADER_SERVICE_NAME': serviceName - } - } - - // JSON body to send with POST request - const body = { - "data": { - "ACCOUNT_NAME": account, - "LOGIN_NAME": user, - "PORT": port, - "PROTOCOL": protocol, - "AUTHENTICATOR": authenticator, - "BROWSER_MODE_REDIRECT_PORT": callback_port.toString() - } - }; - - // Post request to get the SSO URL - return axios - .post(url, body, { - headers: header - }) - .then((response) => - { - var data = response['data']['data']; - proofKey = data['proofKey']; - - return data['ssoUrl']; - }) - .catch(requestErr => - { - throw requestErr; - }); - }; - /** * Parse the GET request and get token parameter value. * @@ -228,7 +173,7 @@ function auth_web(host, browserActionTimeout, webbrowser, httpclient, ) { const withBrowserActionTimeout = (millis, promise) => { const timeout = new Promise((resolve, reject) => setTimeout( - () => reject(`Browser action timed out after ${browserTimeout} ms.`), + () => reject(`Browser action timed out after ${browserActionTimeout} ms.`), millis)); return Promise.race([ promise, diff --git a/lib/authentication/authentication.js b/lib/authentication/authentication.js index 6972398d3..94ade0831 100644 --- a/lib/authentication/authentication.js +++ b/lib/authentication/authentication.js @@ -67,39 +67,29 @@ exports.formAuthJSON = function formAuthJSON( * * @returns {Object} the authenticator. */ -exports.getAuthenticator = function getAuthenticator(connectionConfig) +exports.getAuthenticator = function getAuthenticator(connectionConfig, ssoUrlProvider) { var auth = connectionConfig.getAuthenticator(); - if (auth == authenticationTypes.DEFAULT_AUTHENTICATOR) - { + if (auth == authenticationTypes.DEFAULT_AUTHENTICATOR) { return new auth_default(connectionConfig.password); + } else if (auth == authenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR) { + return new auth_web(connectionConfig, ssoUrlProvider); } - else if (auth == authenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR) - { - return new auth_web(connectionConfig.host, connectionConfig.getBrowserActionTimeout()); - } - if (auth == authenticationTypes.KEY_PAIR_AUTHENTICATOR) - { + if (auth == authenticationTypes.KEY_PAIR_AUTHENTICATOR) { return new auth_keypair(connectionConfig.getPrivateKey(), connectionConfig.getPrivateKeyPath(), connectionConfig.getPrivateKeyPass()); - } - else if (auth == authenticationTypes.OAUTH_AUTHENTICATOR) - { + } else if (auth == authenticationTypes.OAUTH_AUTHENTICATOR) { return new auth_oauth(connectionConfig.getToken()); - } - else if (auth.startsWith('HTTPS://')) - { + } else if (auth.startsWith('HTTPS://')) { return new auth_okta(connectionConfig.password, connectionConfig.region, connectionConfig.account, connectionConfig.getClientType(), connectionConfig.getClientVersion() ); - } - else - { + } else { // Authenticator specified does not exist return new auth_default(connectionConfig.password); } diff --git a/lib/connection/connection.js b/lib/connection/connection.js index 5822b4137..5bda06ed1 100644 --- a/lib/connection/connection.js +++ b/lib/connection/connection.js @@ -236,7 +236,7 @@ function Connection(context) } // Get authenticator to use - var auth = Authenticator.getAuthenticator(connectionConfig); + var auth = Authenticator.getAuthenticator(connectionConfig, context.getServices().ssoUrlProvider); try { @@ -297,7 +297,7 @@ function Connection(context) var self = this; // Get authenticator to use - var auth = Authenticator.getAuthenticator(connectionConfig); + var auth = Authenticator.getAuthenticator(connectionConfig, context.getServices().ssoUrlProvider); try { diff --git a/lib/connection/connection_context.js b/lib/connection/connection_context.js index 111118ea1..93c65970f 100644 --- a/lib/connection/connection_context.js +++ b/lib/connection/connection_context.js @@ -6,6 +6,7 @@ var Util = require('../util'); var Errors = require('../errors'); var SfService = require('../services/sf'); var LargeResultSetService = require('../services/large_result_set'); +var SsoUrlProvider = require('../services/sso_url_provider'); /** * Creates a new ConnectionContext. @@ -38,7 +39,8 @@ function ConnectionContext(connectionConfig, httpClient, config) var services = { sf: new SfService(connectionConfig, httpClient, sfServiceConfig), - largeResultSet: new LargeResultSetService(connectionConfig, httpClient) + largeResultSet: new LargeResultSetService(connectionConfig, httpClient), + ssoUrlProvider: new SsoUrlProvider(connectionConfig, httpClient) }; /** diff --git a/lib/services/sso_url_provider.js b/lib/services/sso_url_provider.js new file mode 100644 index 000000000..c249d7b2e --- /dev/null +++ b/lib/services/sso_url_provider.js @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved. + */ + +const Util = require('../util'); +const Errors = require('../errors'); +const HttpClient = require("../http/node"); +const axios = require("axios"); +const {rest} = require("../global_config"); + +/** + * Creates a new instance of an LargeResultSetService. + * + * @param {Object} connectionConfig + * @param {Object} httpClient + * @constructor + */ +function SsoUrlProvider(connectionConfig, httpClient) { + // validate input + Errors.assertInternal(Util.isObject(connectionConfig)); + Errors.assertInternal(Util.isObject(httpClient)); + + const port = rest.HTTPS_PORT; + const protocol = rest.HTTPS_PROTOCOL; + + /** + * Get SSO URL through POST request. + * + * @param {String} authenticator + * @param {String} serviceName + * @param {String} account + * @param {Number} callback_port + * @param {String} user + * @param {String} host + * + * @returns {String} the SSO URL. + */ + this.getSSOURL = function (authenticator, serviceName, account, callback_port, user, host) { + // Create URL to send POST request to + const url = protocol + '://' + host + "/session/authenticator-request"; + + let header; + if (serviceName) { + header = { + 'HTTP_HEADER_SERVICE_NAME': serviceName + } + } + const body = { + "data": { + "ACCOUNT_NAME": account, + "LOGIN_NAME": user, + "PORT": port, + "PROTOCOL": protocol, + "AUTHENTICATOR": authenticator, + "BROWSER_MODE_REDIRECT_PORT": callback_port.toString() + } + }; + + const httpsClient = new HttpClient(connectionConfig) + const agent = httpsClient.getAgent(url, connectionConfig.getProxy()); + + const requestOptions = + { + method: 'post', + url: url, + headers: header, + data: body, + requestOCSP: false, + rejectUnauthorized: true, + httpsAgent: agent + }; + + // Post request to get the SSO URL + return axios.request(requestOptions) + .then((response) => { + const data = response['data']['data']; + return data; + }) + .catch(requestErr => { + throw requestErr; + }); + }; +} + +module.exports = SsoUrlProvider; diff --git a/test/unit/authentication/authentication_test.js b/test/unit/authentication/authentication_test.js index c2a1bd3dd..6d91c8218 100644 --- a/test/unit/authentication/authentication_test.js +++ b/test/unit/authentication/authentication_test.js @@ -64,7 +64,6 @@ describe('default authentication', function () describe('external browser authentication', function () { var webbrowser; - var httpclient; var browserRedirectPort; const mockProofKey = 'mockProofKey'; @@ -73,6 +72,10 @@ describe('external browser authentication', function () const credentials = connectionOptionsExternalBrowser; const BROWSER_ACTION_TIMEOUT = 10000; + const connectionConfig= { + getBrowserActionTimeout: () => BROWSER_ACTION_TIMEOUT, + host: 'fakehost' + } before(function () { @@ -86,41 +89,35 @@ describe('external browser authentication', function () return; } }); - mock('httpclient', { - post: async function (url, body, header) - { - var data = - { - data: { - data: - { - ssoUrl: mockSsoURL, - proofKey: mockProofKey - } + mock('ssoUrlProvider', { + getSSOURL: async function (authenticator, serviceName, account, callback_port, user, host) { + const data = + { + ssoUrl: mockSsoURL, + proofKey: mockProofKey } - } - browserRedirectPort = body['data']['BROWSER_MODE_REDIRECT_PORT']; + browserRedirectPort = callback_port.toString(); return data; } }); webbrowser = require('webbrowser'); - httpclient = require('httpclient'); + ssoUrlProvider = require('ssoUrlProvider'); }); it('external browser - authenticate method is thenable', done => { - const auth = new auth_web('', BROWSER_ACTION_TIMEOUT, webbrowser.open, httpclient); + const auth = new auth_web(connectionConfig, ssoUrlProvider, webbrowser.open); - auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username) + auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username, credentials.host) .then(done) .catch(done); }); it('external browser - get success', async function () { - const auth = new auth_web('', BROWSER_ACTION_TIMEOUT, webbrowser.open, httpclient); - await auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username); + const auth = new auth_web(connectionConfig, ssoUrlProvider, webbrowser.open); + await auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username, credentials.host); var body = { data: {} }; auth.updateBody(body); @@ -141,28 +138,22 @@ describe('external browser authentication', function () return; } }); - mock('httpclient', { - post: async function (url, body, header) - { - var data = - { - data: { - data: - { - ssoUrl: mockSsoURL - } + mock('ssoUrlProvider', { + getSSOURL: async function (authenticator, serviceName, account, callback_port, user, host) { + const data = + { + ssoUrl: mockSsoURL } - } - browserRedirectPort = body['data']['BROWSER_MODE_REDIRECT_PORT']; + browserRedirectPort = callback_port.toString(); return data; } }); webbrowser = require('webbrowser'); - httpclient = require('httpclient'); + ssoUrlProvider = require('ssoUrlProvider'); - const auth = new auth_web('', BROWSER_ACTION_TIMEOUT, webbrowser.open, httpclient); - await auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username); + const auth = new auth_web(connectionConfig, ssoUrlProvider, webbrowser.open); + await auth.authenticate(credentials.authenticator, '', credentials.account, credentials.username, credentials.host); var body = { data: {} }; auth.updateBody(body);