Skip to content

Commit

Permalink
SNOW-334890: External browser SSO authentication with proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-pmotacki committed Oct 3, 2023
1 parent 39abad3 commit afe40b3
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 125 deletions.
85 changes: 15 additions & 70 deletions lib/authentication/auth_web.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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}`);
Expand All @@ -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.';

Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
};

/**
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 8 additions & 18 deletions lib/authentication/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/connection/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down
4 changes: 3 additions & 1 deletion lib/connection/connection_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
};

/**
Expand Down
85 changes: 85 additions & 0 deletions lib/services/sso_url_provider.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit afe40b3

Please sign in to comment.