Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-334890: External browser SSO authentication with proxy #659

Merged
merged 3 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 17 additions & 70 deletions lib/authentication/auth_web.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,27 @@
*/

const util = require('../util');
const rest = require('../global_config').rest;

const net = require('net');
const querystring = require('querystring');
const URLUtil = require('./../../lib/url_util');
const Util = require('./../../lib/util');
const SsoUrlProvider = require('../authentication/sso_url_provider');

/**
* 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, httpClient, webbrowser) {

const host = connectionConfig.host;
const browserActionTimeout = connectionConfig.getBrowserActionTimeout();
const ssoUrlProvider = new SsoUrlProvider(httpClient);

if (!Util.exists(host)) {
throw new Error(`Invalid value for host: ${host}`);
Expand All @@ -31,14 +33,9 @@ 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 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,13 +76,17 @@ 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);

proofKey = ssoData['proofKey'];

// Step 2: validate URL
let ssoURL = ssoData['ssoUrl'];
if (!URLUtil.isValidURL(ssoURL)) {
throw new Error(util.format("Invalid SSO URL found - %s ", ssoURL));
}
Expand All @@ -94,8 +95,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 +140,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 +175,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
46 changes: 17 additions & 29 deletions lib/authentication/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* Copyright (c) 2015-2021 Snowflake Computing Inc. All rights reserved.
*/

var auth_default = require('./auth_default');
var auth_web = require('./auth_web');
var auth_keypair = require('./auth_keypair');
var auth_oauth = require('./auth_oauth');
var auth_okta = require('./auth_okta');
const auth_default = require('./auth_default');
const auth_web = require('./auth_web');
const auth_keypair = require('./auth_keypair');
const auth_oauth = require('./auth_oauth');
const auth_okta = require('./auth_okta');

var authenticationTypes =
const authenticationTypes =
{
DEFAULT_AUTHENTICATOR: 'SNOWFLAKE', // default authenticator name
EXTERNAL_BROWSER_AUTHENTICATOR: 'EXTERNALBROWSER',
Expand Down Expand Up @@ -37,9 +37,8 @@ exports.formAuthJSON = function formAuthJSON(
clientType,
clientVersion,
clientEnv
)
{
var body =
) {
const body =
{
data:
{
Expand Down Expand Up @@ -67,39 +66,28 @@ exports.formAuthJSON = function formAuthJSON(
*
* @returns {Object} the authenticator.
*/
exports.getAuthenticator = function getAuthenticator(connectionConfig)
{
var auth = connectionConfig.getAuthenticator();
exports.getAuthenticator = function getAuthenticator(connectionConfig, httpClient) {
const 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, httpClient);
}
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,
sfc-gh-pmotacki marked this conversation as resolved.
Show resolved Hide resolved
connectionConfig.region,
connectionConfig.account,
connectionConfig.getClientType(),
connectionConfig.getClientVersion()
);
}
else
{
} else {
// Authenticator specified does not exist
return new auth_default(connectionConfig.password);
}
Expand Down
76 changes: 76 additions & 0 deletions lib/authentication/sso_url_provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved.
*/

const Util = require('../util');
const Errors = require('../errors');
const { rest } = require('../global_config');

/**
* Creates a new instance of an SsoUrlProvider.
*
* @param {Object} httpClient
* @constructor
*/
function SsoUrlProvider(httpClient) {

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} callbackPort
* @param {String} user
* @param {String} host
*
* @returns {String} the SSO URL.
*/
this.getSSOURL = function (authenticator, serviceName, account, callbackPort, 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': callbackPort.toString()
}
};

const requestOptions =
{
method: 'post',
url: url,
headers: header,
data: body,
responseType: 'json'
};

// Post request to get the SSO URL
return httpClient.requestAsync(requestOptions)
.then((response) => {
const data = response['data']['data'];
return data;
})
.catch(requestErr => {
throw requestErr;
});
};
}

module.exports = SsoUrlProvider;
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);
const auth = Authenticator.getAuthenticator(connectionConfig, context.getHttpClient());

try
{
Expand Down Expand Up @@ -297,7 +297,7 @@ function Connection(context)
var self = this;

// Get authenticator to use
var auth = Authenticator.getAuthenticator(connectionConfig);
const auth = Authenticator.getAuthenticator(connectionConfig, context.getHttpClient());

try
{
Expand Down
9 changes: 9 additions & 0 deletions lib/connection/connection_context.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ function ConnectionContext(connectionConfig, httpClient, config)
}
};
};
/**
* Returns instance of httpClient
*
* @returns {NodeHttpClient}
*/
this.getHttpClient = function ()
{
return httpClient;
};
}

module.exports = ConnectionContext;
Loading
Loading