snowflake-sdk
Version:
Node.js driver for Snowflake
255 lines • 9.18 kB
JavaScript
const util = require('../util');
const rest = require('../global_config').rest;
const Logger = require('../logger');
/**
* Creates an okta authenticator.
*
* @param {Object} connectionConfig
* @param {HttpClient} httpClient
*
* @returns {Object}
* @constructor
*/
function AuthOkta(connectionConfig, httpClient) {
const password = connectionConfig.password;
const region = connectionConfig.region;
const account = connectionConfig.account;
const clientAppId = connectionConfig.getClientType();
const clientAppVersion = connectionConfig.getClientVersion();
const host = util.constructHostname(region, account);
const port = rest.HTTPS_PORT;
const protocol = rest.HTTPS_PROTOCOL;
let user;
let ssoUrl;
let tokenUrl;
let samlResponse;
/**
* Update JSON body with saml response.
*
* @param {JSON} body
*
* @returns {null}
*/
this.updateBody = function (body) {
body['data']['RAW_SAML_RESPONSE'] = samlResponse;
};
/**
* Obtain saml response from Okta.
*
* @param {String} authenticator
* @param {String} serviceName
* @param {String} account
* @param {String} username
*
* @returns {null}
*/
this.authenticate = async function (authenticator, serviceName, account, username) {
const response = await getAuthURLs(authenticator, serviceName, account, username);
const responseData = response['data'];
const success = responseData['success'];
const errorCode = responseData['code'];
const errorMessage = responseData['message'];
user = username;
if (typeof success === 'undefined' ||
errorCode === 'undefined' ||
errorMessage === 'undefined') {
throw new Error('Unable to use provided Okta address as an authenticator. Is the authenticator URL correct?');
}
if (success !== true) {
throw new Error(`Unable to use provided Okta address as an authenticator. Error code: ${errorCode}, error message: ${errorMessage}`);
}
ssoUrl = responseData['data']['ssoUrl'];
tokenUrl = responseData['data']['tokenUrl'];
this.validateURLs(authenticator, ssoUrl, tokenUrl);
const responseHtml = await getSAMLResponse(await createAccessToken(tokenUrl, username, password), ssoUrl);
validateSAML(responseHtml);
};
this.reauthenticate = async function (body, retryOption) {
const maxRetryTimeout = connectionConfig.getRetryTimeout();
const maxRetryCount = connectionConfig.getRetrySfMaxLoginRetries();
const remainingTimeout = (maxRetryTimeout - retryOption.totalElapsedTime) * 1000;
const startTime = Date.now();
const authRetryOption = {
maxRetryCount,
numRetries: retryOption.numRetries,
startTime,
remainingTimeout,
maxRetryTimeout,
};
let responseHtml;
while (util.shouldRetryOktaAuth(authRetryOption)) {
try {
responseHtml = await getSAMLResponse(await createAccessToken(tokenUrl, user, password), ssoUrl);
break;
}
catch (err) {
Logger.getInstance().debug('getSAMLResponse: refresh token for re-authentication');
authRetryOption.numRetries++;
}
}
if (remainingTimeout !== 0 && startTime + remainingTimeout < Date.now()) {
Logger.getInstance().warn(`getSAMLResponse: Fail to get SAML response, timeout reached: ${remainingTimeout} miliseconds`);
throw new Error('Reached out to the Login Timeout');
}
if (maxRetryCount < authRetryOption.numRetries) {
Logger.getInstance().warn(`getSAMLResponse: Fail to get SAML response, max retry reached: ${maxRetryCount} time`);
throw new Error('Reached out to the max retry count');
}
retryOption.totalElapsedTime += (Date.now() - startTime) / 1000;
retryOption.numRetries = authRetryOption.numRetries;
validateSAML(responseHtml);
this.updateBody(body);
};
/**
*
* @param {String} authenticator
* @param {String} serviceName
* @param {String} account
* @param {String} username
*
* @returns {Object}
*/
async function getAuthURLs(authenticator, serviceName, account, username) {
// 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: username,
PORT: port,
PROTOCOL: protocol,
AUTHENTICATOR: authenticator,
CLIENT_APP_ID: clientAppId,
CLIENT_APP_VERSION: clientAppVersion,
},
};
// POST request to get SSO URL and token URL
return await httpClient.post(url, body, {
headers: header,
});
}
/**
*
* @param {String} authenticator
* @param {String} ssoUrl
* @param {String} tokenUrl
*
* @returns {null}
*/
this.validateURLs = function (authenticator, ssoUrl, tokenUrl) {
const compareUrlsByProtocolAndHost = (firstUrl, secondUrl) => firstUrl.protocol === secondUrl.protocol && firstUrl.host === secondUrl.host;
try {
const aUrl = new URL(authenticator);
const sUrl = new URL(ssoUrl);
const tUrl = new URL(tokenUrl);
if (!(compareUrlsByProtocolAndHost(aUrl, sUrl) && compareUrlsByProtocolAndHost(aUrl, tUrl))) {
throw new Error('The prefix of the SSO/token URL and the specified authenticator do not match.');
}
}
catch (err) {
// we did not get a valid URL to test
if (err instanceof TypeError) {
throw new Error('Authenticator, SSO, or token URL is invalid.');
}
else {
throw err;
}
}
};
/**
*
* @param {String} tokenUrl
* @param {String} username
* @param {String} password
*
* @returns {Object}
*/
async function createAccessToken(tokenUrl, username, password) {
// JSON body to send with POST request
const body = {
username: username,
password: password,
};
// Query IDP token url to authenticate and retrieve access token
const response = await httpClient.post(tokenUrl, body);
const data = response['data'];
let oneTimeToken;
if (data['sessionToken']) {
oneTimeToken = data['sessionToken'];
}
else {
oneTimeToken = data['cookieToken'];
}
return oneTimeToken;
}
/**
*
* @param {String} oneTimeToken
* @param {String} ssoUrl
*
* @returns {Object}
*/
async function getSAMLResponse(oneTimeToken, ssoUrl) {
// Query IDP URL to get SAML response
const response = await httpClient.get(ssoUrl, {
params: {
RelayState: '/some/deep/link',
onetimetoken: oneTimeToken,
},
});
return response['data'];
}
/**
*
* @param {String} responseHtml
*
* @returns {null}
*/
function validateSAML(responseHtml) {
const postBackUrl = getPostBackUrlFromHtml(responseHtml);
const fullUrl = util.format('%s://%s:%s', protocol, host, port);
// Validate the post back url come back with the SAML response
// contains the same prefix as the Snowflake's server url, which is the
// intended destination url to Snowflake.
if (!connectionConfig.getDisableSamlURLCheck()) {
if (postBackUrl.substring(0, 20) !== fullUrl.substring(0, 20)) {
throw new Error(util.format('The specified authenticator and destination URL ' +
'in the SAML assertion do not match: expected: %s postback: %s', fullUrl, postBackUrl));
}
}
samlResponse = responseHtml;
}
/**
* Extract the postback URL from the HTML response.
*
* @param {String} html
*
* @returns {String}
*/
function getPostBackUrlFromHtml(html) {
const index = html.search('<form');
const startIndex = html.indexOf('action="', index);
const endIndex = html.indexOf('"', startIndex + 8);
return unescapeHtml(html.substring(startIndex + 8, endIndex));
}
/**
* Unescape the HTML hex characters in the string.
*
* @param {String} html
*
* @returns {String}
*/
function unescapeHtml(html) {
return html.replace(/:/g, ':').replace(///g, '/');
}
}
module.exports = AuthOkta;
//# sourceMappingURL=auth_okta.js.map
;