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

Keytartest #664

Closed
wants to merge 9 commits into from
Closed
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
1 change: 1 addition & 0 deletions ci/image/Dockerfile.nodejs-centos7-fips-test
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ RUN chmod +x /usr/local/bin/gosu
RUN yum -y groupinstall 'Development Tools'
RUN yum -y install centos-release-scl
RUN yum -y install devtoolset-8-gcc*
RUN yum -y install libsecret-devel
SHELL [ "/usr/bin/scl", "enable", "devtoolset-8"]

# node-fips environment variables
Expand Down
1 change: 1 addition & 0 deletions ci/image/Dockerfile.nodejs-centos7-node14-test
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ RUN chmod a+x /usr/local/bin/aws

# Development tools + git + zstd + jq + gosu
RUN yum -y groupinstall "Development Tools" && \
yum -y install libsecret-devel && \
yum -y install zlib-devel && \
curl -o - https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.26.0.tar.gz | tar xfz - && \
cd git-2.26.0 && \
Expand Down
2 changes: 1 addition & 1 deletion lib/agent/ocsp_response_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var maxAgeSec = GlobalConfig.getOcspResponseCacheMaxAge();
Errors.assertInternal(Util.number.isPositiveInteger(sizeLimit));
Errors.assertInternal(Util.number.isPositiveInteger(maxAgeSec));

const cacheDir = GlobalConfig.mkdirCacheDir();
const cacheDir = GlobalConfig.mkdirCacheDir(process.env.SF_OCSP_RESPONSE_CACHE_DIR);
const cacheFileName = path.join(cacheDir, "ocsp_response_cache.json");
// create a cache to store the responses, dynamically changes in size
var cache;
Expand Down
37 changes: 37 additions & 0 deletions lib/authentication/auth_idtoken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2015-2023 Snowflake Computing Inc. All rights reserved.
*/

/**
* Creates an oauth authenticator.
*
* @param {String} token
*
* @returns {Object}
* @constructor
*/
function auth_idToken(id_token) {

this.id_token = id_token;

/**
* Update JSON body with token.
*
* @param {JSON} body
*
* @returns {null}
*/
this.updateBody = function (body) {
body['data']['TOKEN'] = this.id_token;
};

this.resetSecret = function () {
this.id_token = null;
};

this.authenticate = async function (authenticator, serviceName, account, username) {
return;
};
}

module.exports = auth_idToken;
16 changes: 13 additions & 3 deletions lib/authentication/authentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ const auth_web = require('./auth_web');
const auth_keypair = require('./auth_keypair');
const auth_oauth = require('./auth_oauth');
const auth_okta = require('./auth_okta');
const auth_idToken = require('./auth_idtoken');
const SecureStorage = require('./secureStorage');

const authenticationTypes =
{
DEFAULT_AUTHENTICATOR: 'SNOWFLAKE', // default authenticator name
EXTERNAL_BROWSER_AUTHENTICATOR: 'EXTERNALBROWSER',
KEY_PAIR_AUTHENTICATOR: 'SNOWFLAKE_JWT',
OAUTH_AUTHENTICATOR: 'OAUTH',
ID_TOKEN_AUTHENTICATOR: 'ID_TOKEN',
MFA_TOEKN_AUTHENTICATOR: 'MFA_TOEKN',
};

exports.authenticationTypes = authenticationTypes;
Expand Down Expand Up @@ -73,14 +77,20 @@ exports.getAuthenticator = function getAuthenticator(connectionConfig, httpClien
return new auth_default(connectionConfig.password);
} else if (auth === authenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR) {
return new auth_web(connectionConfig, httpClient);
}
if (auth === authenticationTypes.KEY_PAIR_AUTHENTICATOR) {
} else if (auth === authenticationTypes.KEY_PAIR_AUTHENTICATOR) {
return new auth_keypair(connectionConfig.getPrivateKey(),
connectionConfig.getPrivateKeyPath(),
connectionConfig.getPrivateKeyPass());
} else if (auth === authenticationTypes.OAUTH_AUTHENTICATOR) {
return new auth_oauth(connectionConfig.getToken());
} else if (auth.startsWith('HTTPS://')) {
}
else if (auth === authenticationTypes.ID_TOKEN_AUTHENTICATOR) {
return new auth_idToken(SecureStorage.ge);
}
else if (auth === authenticationTypes.MFA_TOEKN_AUTHENTICATOR) {
return;
}
else if (auth.startsWith('HTTPS://')) {
return new auth_okta(connectionConfig.password,
connectionConfig.region,
connectionConfig.account,
Expand Down
50 changes: 50 additions & 0 deletions lib/authentication/secureStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

const path = require('path');
const GlobalConfig = require('../global_config');
const Logger = require('../logger');
const keytar = require('keytar');
function createCredentialCacheDir() {
const cacheDirectory = GlobalConfig.mkdirCacheDir(process.env.SF_TEMPORARY_CREDENTIAL_CACHE_DIR);
const credCache = path.join(cacheDirectory, 'temporary_credential.json');
Logger.getInstance().info('Cache directory: ', credCache);
return credCache;
}

/**
*
* @param {String} host
* @param {String} user
* @param {String} cred_type
* @returns
*/
function buildTemporaryCredentialName(host, user, cred_type) {
return `{${host.toUpperCase()}}:{${user.toUpperCase()}}:{SF_NODE_JS_DRIVER}:{${cred_type}}`;
}

async function writeCredential(host, user, credType, token){
if (!token || token == '') {
Logger.getInstance().debug('Token is not provided');
} else {
const result = await keytar.setPassword(host, buildTemporaryCredentialName(host, user, credType), token);

if (!result){
Logger.getInstance().error('Failed to save the credential. Please check whether OS is supporting Local Secure Storage or not.');
}
}
}

async function readCredential(host, user, credType) {
return await keytar.getPassword(host, buildTemporaryCredentialName(host, user, credType));
}

async function deleteCredential(host, user, credType) {
await keytar.deletePassword(host, buildTemporaryCredentialName(host, user, credType));
}

module.exports = {
createCredentialCacheDir,
buildTemporaryCredentialName,
writeCredential,
readCredential,
deleteCredential
};
7 changes: 6 additions & 1 deletion lib/connection/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { v4: uuidv4 } = require('uuid');
const Url = require('url');
const QueryString = require('querystring');
const GSErrors = require('../constants/gs_errors')

const SecureStorage = require('../authentication/secureStorage');
var Util = require('../util');
var Errors = require('../errors');
var ErrorCodes = Errors.codes;
Expand Down Expand Up @@ -330,6 +330,11 @@ function Connection(context)
// Update JSON body with the authentication values
auth.updateBody(body);

if (connectionConfig.getAuthenticator() === Authenticator.awwwuthenticationTypes.EXTERNAL_BROWSER_AUTHENTICATOR &&
connectionConfig.getConsentCacheIdToken()) {
SecureStorage.writeCredential(connectionConfig.host, connectionConfig.username, 'ID_TOKEN', body['data']['TOKEN']);
}

// Request connection
services.sf.connect({
callback: connectCallback(self, callback),
Expand Down
19 changes: 19 additions & 0 deletions lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const DEFAULT_PARAMS =
'forceStageBindError',
'includeRetryReason',
'disableQueryContextCache',
'consentCacheIdToken',
];

function consolidateHostAndAccount(options)
Expand Down Expand Up @@ -503,6 +504,14 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
includeRetryReason = options.includeRetryReason;
}

let consentCacheIdToken = true;
if (Util.exists(options.consentCacheIdToken)) {
Errors.checkArgumentValid(Util.isBoolean(options.includeRetryReason),
ErrorCodes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON);

includeRetryReason = options.includeRetryReason
}

/**
* Returns an object that contains information about the proxy hostname, port,
* etc. for when http requests are made.
Expand Down Expand Up @@ -784,6 +793,15 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
return disableQueryContextCache;
}

/**
* Returns whether idToken cache is enabled or not by the configuration
*
* @returns {Boolean}
*/
this.getConsentCacheIdToken = function () {
return getConsentCacheIdToken
}

/**
* Returns the client config file
*
Expand All @@ -793,6 +811,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
return clientConfigFile;
};


// save config options
this.username = options.username;
this.password = options.password;
Expand Down
1 change: 1 addition & 0 deletions lib/constants/error_messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ exports[404040] = 'Invalid browser timeout value. The specified value must be a
exports[404041] = 'Invalid disablQueryContextCache. The specified value must be a boolean.';
exports[404042] = 'Invalid includeRetryReason. The specified value must be a boolean.'
exports[404043] = 'Invalid clientConfigFile value. The specified value must be a string.';
exports[404044] = 'Invalid consentCacheIdToken. The specified value must be a boolean.'

// 405001
exports[405001] = 'Invalid callback. The specified value must be a function.';
Expand Down
1 change: 1 addition & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ codes.ERR_CONN_CREATE_INVALID_BROWSER_TIMEOUT = 404040;
codes.ERR_CONN_CREATE_INVALID_DISABLED_QUERY_CONTEXT_CACHE = 404041
codes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON =404042
codes.ERR_CONN_CREATE_INVALID_CLIENT_CONFIG_FILE = 404043;
codes.ERR_CONN_CREATE_INVALID_CONSENT_CACHE_ID_TOKEN =404044

// 405001
codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001;
Expand Down
4 changes: 2 additions & 2 deletions lib/global_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ exports.getOcspResponseCacheMaxAge = function ()
*
* @returns {string}
*/
exports.mkdirCacheDir = function ()
exports.mkdirCacheDir = function (envPath)
{
let cacheRootDir = process.env.SF_OCSP_RESPONSE_CACHE_DIR;
let cacheRootDir = envPath;
if (!Util.exists(cacheRootDir))
{
cacheRootDir = os.homedir();
Expand Down
8 changes: 8 additions & 0 deletions lib/parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ names.JS_TREAT_INTEGER_AS_BIGINT = 'JS_TREAT_INTEGER_AS_BIGINT';
names.CLIENT_STAGE_ARRAY_BINDING_THRESHOLD = 'CLIENT_STAGE_ARRAY_BINDING_THRESHOLD';
names.MULTI_STATEMENT_COUNT = 'MULTI_STATEMENT_COUNT';
names.QUERY_CONTEXT_CACHE_SIZE = 'QUERY_CONTEXT_CACHE_SIZE';
names.CLIENT_CONSENT_CACHE_ID_TOKEN = 'CLIENT_CONSENT_CACHE_ID_TOKEN';

var parameters =
[
Expand Down Expand Up @@ -113,6 +114,13 @@ var parameters =
value: 5,
desc: 'Query Context Cache Size'
}),
new Parameter(
{
name: names.CLIENT_CONSENT_CACHE_ID_TOKEN,
value: false,
desc: 'Whether the SSO Token caching is enabled or not'
}
),
];

// put all the parameters in a map so they're easy to retrieve and update
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"glob": "^7.1.6",
"https-proxy-agent": "^5.0.1",
"jsonwebtoken": "^9.0.0",
"keytar": "^7.9.0",
"mime-types": "^2.1.29",
"mkdirp": "^1.0.3",
"moment": "^2.29.4",
Expand Down Expand Up @@ -80,4 +81,4 @@
"url": "https://www.snowflake.com/"
},
"license": "Apache-2.0"
}
}
44 changes: 44 additions & 0 deletions test/unit/authentication/secureStorage_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const assert = require('assert');
const keytar = require('keytar');
const SecureStorage = require('../../../lib/authentication/secureStorage');
const { randomUUID } = require('crypto');

describe('Secure Storage Test', function () {
const host = 'mock_test';
const user = 'mock_user';
const credType = 'MOCK_CREDTYPE';
const randomPassword = randomUUID();
const userNameForStorage = SecureStorage.buildTemporaryCredentialName(host, user, credType);

async function findCredentialFromStorage (userName, password){
const credentialList = await keytar.findCredentials(host);
const result = credentialList.some((element) => {
return element.account === userName && element.password === password;
});
return result;
}

it('test build user name', function (){
assert.strictEqual(userNameForStorage,
'{MOCK_TEST}:{MOCK_USER}:{SF_NODE_JS_DRIVER}:{MOCK_CREDTYPE}'
);
});

it('test - write the mock credential in Local Storage', async function () {
await SecureStorage.writeCredential(host, user, credType, randomPassword);
const result = await findCredentialFromStorage(userNameForStorage, randomPassword);
assert.strictEqual(result, true);
});

it('test - read the mock credential in Local Stoage', async function () {
const savedPassword = await SecureStorage.readCredential(host, user, credType);
assert.strictEqual(savedPassword, randomPassword);
});

it('test - delet the mock credential in Local Storage', async function () {
await SecureStorage.deleteCredential(host, user, credType);
const result = await findCredentialFromStorage(userNameForStorage, randomPassword);
assert.strictEqual(result, false);
});
});

12 changes: 12 additions & 0 deletions test/unit/connection/connection_config_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,18 @@ describe('ConnectionConfig: basic', function ()
},
errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_INCLUDE_RETRY_REASON,
},
{

name: 'invalid consentCacheIdToken',
options:
{
username: 'username',
password: 'password',
account: 'account',
consentCacheIdToken: 'invalid'
},
errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_CONSENT_CACHE_ID_TOKEN,
},
{
name: 'invalid clientConfigFile',
options: {
Expand Down