UNPKG

snowflake-sdk

Version:
284 lines 15.2 kB
"use strict"; 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 open = require('open'); const Util = require('../util'); const AuthenticationTypes = require('./authentication_types'); const { coordinateAuth } = require('./auth_coordinator'); /** * 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 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.CLIENT_ENVIRONMENT.OAUTH_TYPE = AuthenticationTypes.OAUTH_AUTHORIZATION_CODE; }; this.loadOauth4webapi = async function () { if (!oauth) { oauth = await import('oauth4webapi'); } }; this.authenticate = async function () { globalThis.crypto ??= require('node:crypto').webcrypto; token = await coordinateAuth(connectionConfig.host, connectionConfig.username, AuthenticationTypes.OAUTH_AUTHORIZATION_CODE, async () => { const accessTokenFromCache = await readCache(accessTokenKey); const refreshTokenFromCache = await readCache(refreshTokenKey); if (accessTokenFromCache && connectionConfig.getClientStoreTemporaryCredential()) { return accessTokenFromCache; } else if (refreshTokenFromCache && connectionConfig.getClientStoreTemporaryCredential()) { try { return await this.getAccessTokenUsingRefreshToken(refreshTokenFromCache); } catch (error) { // A cached refresh token can be revoked, expired, or already // consumed (single-use). Without eviction the bad token stays in // the cache and every subsequent connect repeats this failure with // no recovery path. await removeFromCache(refreshTokenKey); Logger().warn(format('Error while getting access token using cached refresh token. Message: %s. The refresh token is removed from cache - authentication will proceed from the beginning', error.message)); return this.executeFullAuthorizationCodeFlow(); } } else { return this.executeFullAuthorizationCodeFlow(); } }); }; // Wipes the cached access token. Called by the connection-failure dispatch // in `sf.js` for every GS `success: false` response so we never resend a // stale access token, regardless of whether a full reauthenticate follows. this.clearAccessTokenCache = async function () { await removeFromCache(accessTokenKey); }; this.reauthenticate = async function (body) { const refreshToken = await readCache(refreshTokenKey); if (refreshToken) { try { token = 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 }; // oxlint-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(); // oxlint-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 { 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'); if (scope) { 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, { renderer: connectionConfig.browserResponseRenderer, }); }); const redirectUri = new URL(authorizationUrl.searchParams.get('redirect_uri')); await verifyPortIsAvailable(server, redirectUri.port); try { await new Promise((resolve, reject) => { server.once('listening', resolve); server.once('error', reject); server.listen(redirectUri.port || 0, 0); }); } catch (err) { throw new Error(`Error while creating local server on port ${redirectUri.port}: ${err}`); } try { const codeProvider = connectionConfig.openExternalBrowserCallback || open; Logger().debug(`Opening your browser to obtain the authorization code: ${authorizationUrl.href}`); const result = codeProvider(authorizationUrl.href); if (result && typeof result.catch === 'function') { // Fire and forget, the actual result handler is in codeResponse // .catch() is used to avoid unhandled promise rejection warning result.catch(() => { }); } const codeResponse = await withBrowserActionTimeout(browserActionTimeout, receiveData).catch((rejected) => { 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 }; } finally { // createServer closes the server itself on its success / IdP-error // paths; closing again here is a safe no-op and ensures we don't leak // the listener on timeouts, browser-launch failures, or unexpected throws. server.close(); } } async function convertToResponseType(httpClient, url, options) { function asResponseType(response) { return new Response(response.json, { status: 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) { 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 ? { // oxlint-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; } 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}`; } } module.exports = AuthOauthAuthorizationCode; //# sourceMappingURL=auth_oauth_authorization_code.js.map