snowflake-sdk
Version:
Node.js driver for Snowflake
261 lines • 13.9 kB
JavaScript
const Logger = require('./../logger').default;
const authUtil = require('../authentication/authentication_util');
const { getFreePort, format } = require('../util');
const { withBrowserActionTimeout, writeToCache, removeFromCache, readCache, } = require('./authentication_util');
const querystring = require('querystring');
const GlobalConfig = require('../global_config');
const open = require('open');
const Util = require('../util');
const AuthenticationTypes = require('./authentication_types');
/**
* Creates an oauth authenticator.
*
* @param {Object} connectionConfig
* @param {Object} httpClient
*
* @returns {Object}
* @constructor
*/
function AuthOauthAuthorizationCode(connectionConfig, httpClient) {
const DEFAULT_REDIRECT_HOST = 'http://127.0.0.1';
const DEFAULT_REDIRECT_URI_ENDPOINT = '/';
const browserActionTimeout = connectionConfig.getBrowserActionTimeout();
let oauth;
let token;
const clientId = connectionConfig.getOauthClientId();
const clientSecret = connectionConfig.getOauthClientSecret();
const authorizationUrl = getAuthorizationUrl(connectionConfig);
const tokenUrl = authUtil.getTokenUrl(connectionConfig);
const accessTokenKey = authUtil.buildOauthAccessTokenCacheKey(authorizationUrl.host, connectionConfig.username, AuthenticationTypes.OAUTH_AUTHORIZATION_CODE);
const refreshTokenKey = authUtil.buildOauthRefreshTokenCacheKey(tokenUrl.host, connectionConfig.username, AuthenticationTypes.OAUTH_AUTHORIZATION_CODE);
/**
* Update JSON body with token.
* @param {JSON} body
* @returns {null}
*/
this.updateBody = function (body) {
if (token) {
body['data']['TOKEN'] = token;
}
body['data']['AUTHENTICATOR'] = AuthenticationTypes.OAUTH_AUTHENTICATOR;
body['data']['OAUTH_TYPE'] = AuthenticationTypes.OAUTH_AUTHORIZATION_CODE;
};
this.loadOauth4webapi = async function () {
if (!oauth) {
oauth = await Util.dynamicImportESMInTypescriptWithCommonJS('oauth4webapi');
}
};
this.authenticate = async function () {
globalThis.crypto ??= require('node:crypto').webcrypto;
//verify that there is access token in the cache
const accessTokenFromCache = await readCache(accessTokenKey);
//verify that there is refresh token in the cache
const refreshTokenFromCache = await readCache(refreshTokenKey);
if (accessTokenFromCache && connectionConfig.getClientStoreTemporaryCredential()) {
token = accessTokenFromCache;
}
else if (refreshTokenFromCache && connectionConfig.getClientStoreTemporaryCredential()) {
token = await this.getAccessTokenUsingRefreshToken(refreshTokenFromCache);
}
else {
token = await this.executeFullAuthorizationCodeFlow();
}
};
this.reauthenticate = async function (body) {
await removeFromCache(accessTokenKey);
const refreshToken = await readCache(refreshTokenKey);
if (refreshToken) {
try {
await this.getAccessTokenUsingRefreshToken(refreshToken);
this.updateBody(body);
}
catch (error) {
await removeFromCache(refreshTokenKey);
Logger().warn(format('Error while getting access token using refresh token. Message: %s. The refresh token is removed form cache - authentication must be proceed from the beginning', error.message));
await this.authenticate();
this.updateBody(body);
}
}
else {
await this.authenticate();
this.updateBody(body);
}
};
this.executeFullAuthorizationCodeFlow = async function () {
await this.loadOauth4webapi(); // import module using the dynamic import
const codeChallengeMethod = connectionConfig.getOauthChallengeMethod() || 'S256'; // TODO: should be verified with "discovery" response
//An issuer is a obligatory parameter in validation processed by oauth4webapi library, even when it isn't used
const issuer = connectionConfig.issuer || 'UNKNOWN';
const codeVerifier = oauth.generateRandomCodeVerifier();
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
const as = { issuer: issuer };
// eslint-disable-next-line camelcase
const client = { client_id: clientId };
const clientAuth = oauth.ClientSecretPost(clientSecret);
const redirectUri = await buildRedirectUri(connectionConfig);
const scope = await authUtil.prepareScope(connectionConfig);
const authorizationUrlWithParams = await prepareAuthorizationUrl(authorizationUrl, client, redirectUri, codeChallenge, codeChallengeMethod, as, scope);
const authorizationCodeResponse = await requestAuthorizationCode(authorizationUrlWithParams, browserActionTimeout);
const params = oauth.validateAuthResponse(as, client, authorizationUrlWithParams, authorizationCodeResponse.state);
params.set('code', authorizationCodeResponse.code);
Logger().trace('Requesting token');
const token = await requestToken(as, tokenUrl, client, clientAuth, params, redirectUri, codeVerifier);
return token;
};
this.getAccessTokenUsingRefreshToken = async function (refreshToken) {
globalThis.crypto ??= require('node:crypto').webcrypto;
await this.loadOauth4webapi(); // import module using the dynamic import
const issuer = connectionConfig.issuer || 'UNKNOWN';
const as = { issuer: issuer };
const clientId = connectionConfig.getOauthClientId();
const clientSecret = connectionConfig.getOauthClientSecret();
// eslint-disable-next-line camelcase
const client = { client_id: clientId };
const clientAuth = oauth.ClientSecretPost(clientSecret);
// Refresh Token Grant Request & Response
const tokenUrl = authUtil.getTokenUrl(connectionConfig);
Logger().trace(`Receiving new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
as['token_endpoint'] = tokenUrl.href;
const response = await oauth.refreshTokenGrantRequest(as, client, clientAuth, refreshToken, {
[oauth.allowInsecureRequests]: connectionConfig.getOauthHttpAllowed(),
[oauth.customFetch]: async (url, options) => await convertToResponseType(httpClient, url, options),
});
const result = await oauth.processRefreshTokenResponse(as, client, response);
if (result.access_token) {
//cache access token
Logger().debug(`Received new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
await writeToCache(accessTokenKey, result.access_token);
//cache refreshToken if exists
if (result.refresh_token) {
//cache refresh token
Logger().debug(`Received new OAuth refresh token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
await writeToCache(refreshTokenKey, result.refresh_token);
}
else {
Logger().warn('There is no refresh_token value to write to cache. Clearing refresh token in cache');
await removeFromCache(refreshTokenKey);
}
}
else {
throw Error(`Response doesn't contain OAuth access token. Requested URI: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
}
return result.access_token;
};
async function prepareAuthorizationUrl(authorizationUrl, client, redirectUri, codeChallenge, codeChallengeMethod, as, scope) {
authorizationUrl.searchParams.set('client_id', client.client_id);
authorizationUrl.searchParams.set('redirect_uri', redirectUri);
authorizationUrl.searchParams.set('response_type', 'code');
authorizationUrl.searchParams.set('scope', scope);
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
/**
* We cannot be sure PKCE is supported then the state should be used.
*/
if (as.code_challenge_methods_supported?.includes('S256') !== true) {
const state = oauth.generateRandomState();
authorizationUrl.searchParams.set('state', state);
}
return authorizationUrl;
}
async function verifyPortIsAvailable(server, redirectPort) {
return Util.isPortOpen(redirectPort).catch((rejected) => {
server.close();
throw new Error(`Cannot run server using provided redirect url. ${rejected}`);
});
}
async function requestAuthorizationCode(authorizationUrl, browserActionTimeout) {
if (!Util.number.isPositiveInteger(browserActionTimeout)) {
throw new Error(`Invalid value for browser action timeout: ${browserActionTimeout}`);
}
let server;
const receiveData = new Promise((resolve, reject) => {
server = authUtil.createServer(resolve, reject);
}).then((result) => {
return result;
});
const redirectUri = new URL(authorizationUrl.searchParams.get('redirect_uri'));
await verifyPortIsAvailable(server, redirectUri.port);
server.listen(redirectUri.port || 0, 0);
const authorizationCodeProvider = GlobalConfig.getCustomRedirectingClient();
const codeProvider = authorizationCodeProvider
? authorizationCodeProvider
: browserAuthorizationCodeProvider;
await codeProvider(authorizationUrl);
const codeResponse = await withBrowserActionTimeout(browserActionTimeout, receiveData).catch((rejected) => {
server.close();
throw new Error(rejected);
});
const autorizationCodeResponseParameters = querystring.parse(codeResponse.substring(codeResponse.indexOf('?') + 1));
const code = autorizationCodeResponseParameters['code'];
const state = autorizationCodeResponseParameters['state'].replace(new RegExp('\\sHTTP/.*'), '');
Logger().debug(`Received new OAuth authorization code from: Host: ${authorizationUrl.host} Path: ${authorizationUrl.pathname}`);
return { code: code, state: state };
}
async function convertToResponseType(httpClient, url, options) {
function asResponseType(response) {
return new Response(response.json, {
staus: response.statusCode,
statusText: response.statusText,
headers: response.headers,
});
}
options.url = url;
return asResponseType(await httpClient.requestAsync(options));
}
async function requestToken(as, tokenUrl, client, clientAuth, params, redirectUri, codeVerifier) {
try {
Logger().trace(`Receiving new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
as['token_endpoint'] = tokenUrl.href;
const response = await oauth.authorizationCodeGrantRequest(as, client, clientAuth, params, redirectUri, codeVerifier, {
[oauth.allowInsecureRequests]: connectionConfig.getOauthHttpAllowed(),
[oauth.customFetch]: async (url, options) => await convertToResponseType(httpClient, url, options),
additionalParameters: connectionConfig.oauthEnableSingleUseRefreshTokens
? {
// eslint-disable-next-line camelcase
enable_single_use_refresh_tokens: 'true',
}
: undefined,
});
const result = await oauth.processAuthorizationCodeResponse(as, client, response);
if (result.access_token) {
//cache access token
Logger().debug(`Received new OAuth access token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
await writeToCache(accessTokenKey, result.access_token);
//cache refreshToken if exists
if (result.refresh_token) {
//cache refresh token
Logger().debug(`Received new OAuth refresh token from: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
await writeToCache(refreshTokenKey, result.refresh_token);
}
}
else {
throw Error(`Response doesn't contain OAuth access token. Requested URI: Host: ${tokenUrl.host} Path: ${tokenUrl.pathname}`);
}
return result.access_token;
}
catch (error) {
throw new Error(format('Error while getting access token. Message: %s', error.message));
}
}
function getAuthorizationUrl(options) {
const authCodeUrl = options.getOauthAuthorizationUrl();
Logger().debug(`Url used for receiving authorization code: ${authCodeUrl}`);
return new URL(authCodeUrl);
}
async function buildRedirectUri(options) {
const redirectUri = options.getOauthRedirectUri() || (await createDefaultRedirectUri());
Logger().debug(`Authorization code redirect URL: ${redirectUri}`);
return redirectUri;
}
async function createDefaultRedirectUri() {
const redirectPort = await getFreePort();
return `${DEFAULT_REDIRECT_HOST}:${redirectPort}${DEFAULT_REDIRECT_URI_ENDPOINT}`;
}
async function browserAuthorizationCodeProvider(authorizationUrl) {
Logger().debug(`Opening your browser to obtain the authorization code: ${authorizationUrl}`);
return open(authorizationUrl.href);
}
}
module.exports = AuthOauthAuthorizationCode;
//# sourceMappingURL=auth_oauth_authorization_code.js.map
;