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

Key Pair Authentication #131

Closed
wants to merge 3 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
46 changes: 46 additions & 0 deletions lib/connection/auth_keypair.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const crypto = require('crypto');
const jwt = require('jsonwebtoken');

const LIFETIME = 120; // seconds
const ALGORITHM = 'RS256'
const ISSUER = 'iss'
const SUBJECT = 'sub'
const EXPIRE_TIME = 'exp'
const ISSUE_TIME = 'iat'

exports.generateToken = function ({ account, username, privateKey }) {
account = account.toUpperCase();
username = username.toUpperCase();

const privateKeyObj = crypto.createPrivateKey({
key: privateKey,
format: 'der',
type: 'pkcs8'
});

const publicKeyFp = calculatePublicKeyFingerprint(privateKeyObj);

const now = new Date();
const jwtExpiration = new Date(now.getTime());
jwtExpiration.setSeconds(jwtExpiration.getSeconds() + LIFETIME);

const payload = {
[ISSUER]: `${account}.${username}.${publicKeyFp}`,
[SUBJECT]: `${account}.${username}`,
[ISSUE_TIME]: Math.floor(now.getTime() / 1000),
[EXPIRE_TIME]: Math.floor(jwtExpiration.getTime() / 1000),
}

return jwt.sign(payload, privateKeyObj, { algorithm: ALGORITHM });
}

function calculatePublicKeyFingerprint(privateKeyObj)
{
const publicKeyObj = crypto.createPublicKey(privateKeyObj);
const publicKeyBytes = publicKeyObj.export({type: 'spki', format: 'der'});

const sha256Hash = crypto.createHash('sha256').update(publicKeyBytes).digest('base64');
const publicKeyFp = `SHA256:${sha256Hash}`;

return publicKeyFp;
}
32 changes: 26 additions & 6 deletions lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const Errors = require('../errors');
const ErrorCodes = Errors.codes;
const NativeTypes = require('./result/data_types').NativeTypes;
const GlobalConfig = require('../global_config');
const AuthByKeyPair = require('./auth_keypair');

function consolidateHostAndAccount(options)
{
Expand Down Expand Up @@ -110,13 +111,26 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
Errors.checkArgumentValid(Util.isString(options.username),
ErrorCodes.ERR_CONN_CREATE_INVALID_USERNAME);

// check for missing password
Errors.checkArgumentExists(Util.exists(options.password),
ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD);
// if using key pair authentication
if (Util.exists(options.privateKey)) {
// check for valid private key
Errors.checkArgumentValid(Util.isPrivateKey(options.privateKey),
ErrorCodes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY);

// check for invalid password
Errors.checkArgumentValid(Util.isString(options.password),
ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD);
options.authenticator = GlobalConfig.authenticator.KEY_PAIR;
}
// using default password authentication
else {
// check for missing password
Errors.checkArgumentExists(Util.exists(options.password),
ErrorCodes.ERR_CONN_CREATE_MISSING_PASSWORD);

// check for invalid password
Errors.checkArgumentValid(Util.isString(options.password),
ErrorCodes.ERR_CONN_CREATE_INVALID_PASSWORD);

options.authenticator = GlobalConfig.authenticator.DEFAULT;
}

consolidateHostAndAccount(options);
}
Expand Down Expand Up @@ -395,9 +409,15 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo)
return jsTreatIntegerAsBigInt;
};

this.getKeyPairToken = function ()
{
return AuthByKeyPair.generateToken(options);
};

// save config options
this.username = options.username;
this.password = options.password;
this.authenticator = options.authenticator;
this.accessUrl = options.accessUrl;
this.account = options.account;
this.sessionToken = options.sessionToken;
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 @@ -47,6 +47,7 @@ exports[404022] = 'Invalid region. The specified value must be a string.';
exports[404023] = 'Invalid clientSessionKeepAlive. The specified value must be a boolean.';
exports[404024] = 'Invalid clientSessionKeepAliveHeartbeatFrequency. The specified value must be a number.';
exports[404025] = 'Invalid jsTreatIntegerAsBigInt. The specified value must be a boolean';
exports[404026] = 'Invalid private key. Please provide a valid unencrypted rsa private key in DER format with type pkcs8 as a buffer';

// 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 @@ -52,6 +52,7 @@ codes.ERR_CONN_CREATE_INVALID_REGION = 404022;
codes.ERR_CONN_CREATE_INVALID_KEEP_ALIVE = 404023;
codes.ERR_CONN_CREATE_INVALID_KEEP_ALIVE_HEARTBEAT_FREQ = 404024;
codes.ERR_CONN_CREATE_INVALID_TREAT_INTEGER_AS_BIGINT = 404025;
codes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY = 404026;

// 405001
codes.ERR_CONN_CONNECT_INVALID_CALLBACK = 405001;
Expand Down
6 changes: 6 additions & 0 deletions lib/global_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,9 @@ exports.mkdirCacheDir = function ()
}
return cacheDir;
};

const authenticator = {
DEFAULT: 'SNOWFLAKE',
KEY_PAIR: 'SNOWFLAKE_JWT',
}
exports.authenticator = authenticator;
7 changes: 7 additions & 0 deletions lib/services/sf.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const Url = require('url');
const QueryString = require('querystring');
const Parameters = require('../parameters');
const GSErrors = require('../constants/gs_errors')
const GlobalConfig = require('../global_config');

const Logger = require('../logger');

Expand Down Expand Up @@ -1021,6 +1022,12 @@ StateConnecting.prototype.continue = function (options)
PASSWORD: this.connectionConfig.password
}
};

if (this.connectionConfig.authenticator === GlobalConfig.authenticator.KEY_PAIR)
{
json.data.AUTHENTICATOR = this.connectionConfig.authenticator;
json.data.TOKEN = this.connectionConfig.getKeyPairToken();
}
}

// extract the inflight context from the error and put it back in the json
Expand Down
14 changes: 14 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

var util = require('util');
var Url = require('url');
var crypto = require('crypto');

/**
* Note: A simple wrapper around util.inherits() for now, but this might change
Expand Down Expand Up @@ -443,3 +444,16 @@ const userAgent = 'JavaScript' + '/' + driverVersion
+ ' (' + process.platform + '-' + process.arch + ') ' + 'NodeJS' + '/' + nodeJSVersion;

exports.userAgent = userAgent;

exports.isPrivateKey = function (privateKey) {
try {
return !!(crypto.createPrivateKey({
key: privateKey,
format: 'der',
type: 'pkcs8'
}));
}
catch (err) {
return false;
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"debug": "^3.2.6",
"extend": "^3.0.2",
"https-proxy-agent": "^3.0.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.15",
"mkdirp": "^1.0.3",
"moment": "^2.23.0",
"moment-timezone": "^0.5.15",
Expand All @@ -20,7 +22,6 @@
"requestretry": "^4.1.0",
"simple-lru-cache": "^0.0.2",
"uuid": "^3.3.2",
"lodash": "^4.17.15",
"winston": "^3.1.0"
},
"devDependencies": {
Expand Down
35 changes: 35 additions & 0 deletions test/integration/connectionOptions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/*
* Copyright (c) 2015-2019 Snowflake Computing Inc. All rights reserved.
*/
const fs = require('fs');
const crypto = require('crypto');

let snowflakeTestProtocol = process.env.SNOWFLAKE_TEST_PROTOCOL;
let snowflakeTestHost = process.env.SNOWFLAKE_TEST_HOST;
let snowflakeTestPort = process.env.SNOWFLAKE_TEST_PORT;
Expand All @@ -15,6 +18,9 @@ const snowflakeTestRole = process.env.SNOWFLAKE_TEST_ROLE;
const snowflakeTestPassword = process.env.SNOWFLAKE_TEST_PASSWORD;
const snowflakeTestAdminUser = process.env.SNOWFLAKE_TEST_ADMIN_USER;
const snowflakeTestAdminPassword = process.env.SNOWFLAKE_TEST_ADMIN_PASSWORD;
let snowflakeTestPrivateKeyUser = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_USER;
const snowflakeTestPrivateKeyPath = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_PATH;
const snowflakeTestPrivateKeyPassphrase = process.env.SNOWFLAKE_TEST_PRIVATE_KEY_PASSPHRASE;

if (snowflakeTestProtocol === undefined)
{
Expand All @@ -41,6 +47,11 @@ if (snowflakeTestProxyPort === undefined)
snowflakeTestProxyPort = '3128';
}

if (snowflakeTestPrivateKeyUser === undefined)
{
snowflakeTestPrivateKeyUser = snowflakeTestUser;
}

const accessUrl = snowflakeTestProtocol + '://' + snowflakeTestHost + ':' +
snowflakeTestPort;

Expand Down Expand Up @@ -82,6 +93,30 @@ var wrongPwd =
account: snowflakeTestAccount
};

var getPrivateKey = function () {
try {
const privateKey = fs.readFileSync(snowflakeTestPrivateKeyPath, 'utf-8');
const privateKeyPEM = crypto.createPrivateKey({
key: privateKey,
format: 'pem',
passphrase: snowflakeTestPrivateKeyPassphrase,
});

return privateKeyPEM.export({ type: 'pkcs8', format: 'der' });
}
catch (err) {
return 'invalid_private_key';
}
};
var validKeyPairAuth =
{
accessUrl: accessUrl,
username: snowflakeTestPrivateKeyUser,
account: snowflakeTestAccount,
privateKey: getPrivateKey(),
};

exports.validKeyPairAuth = validKeyPairAuth;
exports.valid = valid;
exports.snowflakeAccount = snowflakeAccount;
exports.wrongUserName = wrongUserName;
Expand Down
11 changes: 11 additions & 0 deletions test/integration/testConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ describe('Connection test', function ()
);
});

it('Connect using Key Pair', function (done)
{
var connection = snowflake.createConnection(connOption.validKeyPairAuth);
connection.connect(function (err)
{
assert.ok(!err, JSON.stringify(err));
done();
});
});

it('Wrong Username', function (done)
{
var connection = snowflake.createConnection(connOption.wrongUserName);
Expand All @@ -98,6 +108,7 @@ describe('Connection test', function ()
done();
});
});

it('Multiple Client', function (done)
{
const totalConnections = 10;
Expand Down
40 changes: 37 additions & 3 deletions test/unit/connection/connection_config_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
* Copyright (c) 2015 Snowflake Computing Inc. All rights reserved.
*/

var ConnectionConfig = require('./../../../lib/connection/connection_config');
var ErrorCodes = require('./../../../lib/errors').codes;
var assert = require('assert');
const ConnectionConfig = require('./../../../lib/connection/connection_config');
const ErrorCodes = require('./../../../lib/errors').codes;
const assert = require('assert');
const crypto = require('crypto');
const GlobalConfig = require('../../../lib/global_config');

describe('ConnectionConfig: basic', function ()
{
Expand Down Expand Up @@ -253,6 +255,16 @@ describe('ConnectionConfig: basic', function ()
fetchAsString: ['invalid']
},
errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_FETCH_AS_STRING_VALUES
},
{
name: 'invalid privateKey',
options:
{
username: 'username',
privateKey: 'invalid',
account: 'account',
},
errorCode: ErrorCodes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY
}
];

Expand Down Expand Up @@ -508,4 +520,26 @@ describe('ConnectionConfig: basic', function ()
assert.strictEqual(
connectionConfig.getResultPrefetch(), resultPrefetchCustom);
});

it('sets authenticator to key pair if privateKey exists', function () {
const keyPair = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
privateKeyEncoding: {
type: 'pkcs8',
format: 'der'
}
});
const privateKey = keyPair.privateKey;

const connOption =
{
username: 'username',
account: 'account',
privateKey: privateKey,
};

const result = new ConnectionConfig(connOption);

assert.strictEqual(result.authenticator, GlobalConfig.authenticator.KEY_PAIR);
});
});