UNPKG

@simbachain/hardhat

Version:
505 lines 21.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.REALM = exports.AUTH_URL = exports.CLIENT_ID = exports.KeycloakHandler = void 0; const lib_1 = require("../lib"); const chalk_1 = __importDefault(require("chalk")); const axios_1 = __importDefault(require("axios")); const url_1 = require("url"); const CLIENT_ID = "simba-pkce"; exports.CLIENT_ID = CLIENT_ID; const AUTH_URL = "https://simba-dev-sso.platform.simbachain.com"; exports.AUTH_URL = AUTH_URL; const REALM = "simbachain"; exports.REALM = REALM; const KeycloakAuthErrors = { headersError: `${chalk_1.default.red('simba: Error acquiring auth headers. Please make sure keycloak certs are not expired.')}`, keycloakCertsError: `${chalk_1.default.red('simba: Error obtaining certs from keycloak. Please make sure keycloak certs are not expired.')}`, verificationInfoError: `${chalk_1.default.red('simba: Error acquiring verification info. Please make sure keycloak certs are not expired.')}`, authTokenError: `${chalk_1.default.red('simba: Error acquiring auth token. Please make sure keycloak certs are not expired')}`, noClientIDError: `${chalk_1.default.red('simba: Error acquiring clientID. Please make sure "clientID" is configured in simba.json')}`, noBaseURLError: `${chalk_1.default.red('simba: Error acquiring baseURL. Please make sure "baseURL" is configured in simba.json')}`, noAuthURLError: `${chalk_1.default.red('simba: Error acquiring authURL. Please make sure "authURLID" is configured in simba.json')}`, noRealmError: `${chalk_1.default.red('simba: Error acquiring realm. Please make sure "realm" is configured in simba.json')}`, }; class KeycloakHandler { constructor(config, projectConfig, tokenExpirationPad = 60) { this.authErrors = KeycloakAuthErrors; this.config = lib_1.SimbaConfig.ConfigStore; this.projectConfig = lib_1.SimbaConfig.ProjectConfigStore; this.clientID = this.projectConfig.get('clientID') ? this.projectConfig.get('clientID') : this.projectConfig.get('clientId'); if (!this.clientID) { lib_1.log.error(`:: ${this.authErrors.noClientIDError}`); } this.baseURL = this.projectConfig.get('baseURL') ? this.projectConfig.get('baseURL') : this.projectConfig.get('baseUrl'); if (!this.baseURL) { lib_1.log.error(`:: ${this.authErrors.noBaseURLError}`); } this.authURL = this.projectConfig.get('authURL') ? this.projectConfig.get('authURL') : this.projectConfig.get('authUrl'); if (!this.authURL) { lib_1.log.error(`:: ${this.authErrors.noAuthURLError}`); } this.realm = this.projectConfig.get('realm'); if (!this.realm) { lib_1.log.error(`:: ${this.authErrors.noRealmError}`); } this.configBase = this.baseURL.split(".").join("_"); this.tokenExpirationPad = tokenExpirationPad; } getConfigBase() { if (!this.configBase) { this.configBase = this.baseURL.split(".").join("_"); } return this.configBase; } setLoggedInStatus(status) { this._loggedIn = status; } isLoggedIn() { if (this.verificationInfo) { return true; } else { return false; } } async logout() { this.setLoggedInStatus(false); this.deleteAuthInfo(); } deleteAuthInfo() { lib_1.log.debug(`:: ENTER : deleting any existing authToken info`); this.config.set(this.configBase, {}); } getPathToConfigFile() { return this.config.path; } hasConfig(key) { if (!this.config.has(this.configBase)) { return false; } return key in this.config.get(this.getConfigBase()); } getConfig(key) { if (!this.config.has(this.configBase)) { return; } const dict = this.config.get(this.getConfigBase()); if (!(key in dict)) { return; } return dict[key]; } getOrSetConfig(key, value) { if (!this.hasConfig(key)) { this.setConfig(key, value); return value; } return this.getConfig(key); } setConfig(key, value) { lib_1.log.debug(`:: ENTER : KEY: ${key}, VALUE: ${JSON.stringify(value)}`); if (!this.config.has(this.configBase)) { this.config.set(this.configBase, {}); } const dict = this.config.get(this.configBase); dict[key] = value; this.config.set(this.configBase, dict); lib_1.log.debug(`:: EXIT : this.config: ${JSON.stringify(this.config)}`); return value; } deleteConfig(key) { if (!this.config.has(this.configBase)) { return; } const dict = this.config.get(this.configBase); if (!(key in dict)) { return; } delete dict[key]; this.config.set(this.configBase, dict); } async getVerificationInfo() { lib_1.log.debug(`:: ENTER :`); const url = `${this.authURL}/auth/realms/${this.realm}/protocol/openid-connect/auth/device`; const params = new url_1.URLSearchParams(); params.append('client_id', this.clientID); const headers = { 'content-type': 'application/x-www-form-urlencoded;charset=utf-8' }; const config = { headers: headers, }; try { const res = await axios_1.default.post(url, params, config); const verificationInfo = res.data; this.verificationInfo = verificationInfo; lib_1.log.debug(`:: EXIT : ${JSON.stringify(this.verificationInfo)}`); return verificationInfo; } catch (error) { lib_1.log.error(`${chalk_1.default.redBright(`simba: EXIT : ${JSON.stringify(error)}`)}`); return error; } } async loginUser() { lib_1.log.debug(`:: ENTER :`); if (!this.isLoggedIn()) { this.verificationInfo = await this.getVerificationInfo(); } const verificationCompleteURI = this.verificationInfo.verification_uri_complete; // the following line is where we begin the flow of handling not acquiring verification info if (!verificationCompleteURI) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.keycloakCertsError}`)}`); return; } lib_1.log.info(`\n${chalk_1.default.cyanBright('\nsimba: Please navigate to the following URI to log in: ')} ${chalk_1.default.greenBright(verificationCompleteURI)}`); lib_1.log.debug(`:: EXIT :`); return verificationCompleteURI; } async getAuthToken(pollingConfig = { maxAttempts: 60, interval: 3000, }, refreshing = false) { lib_1.log.debug(`:: ENTER :`); const maxAttempts = pollingConfig.maxAttempts; const interval = pollingConfig.interval; if (!this.isLoggedIn()) { this.verificationInfo = await this.getVerificationInfo(); } if (!this.verificationInfo) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.verificationInfoError}`)}`); return; } const deviceCode = this.verificationInfo.device_code; const params = new url_1.URLSearchParams(); const url = `${this.authURL}/auth/realms/${this.realm}/protocol/openid-connect/token`; const headers = { 'content-type': 'application/x-www-form-urlencoded;charset=utf-8' }; const config = { headers: headers, }; if (!refreshing) { params.append("grant_type", "urn:ietf:params:oauth:grant-type:device_code"); params.append("client_id", this.clientID); params.append("device_code", deviceCode); let attempts = 0; while (attempts < maxAttempts) { await new Promise(resolve => setTimeout(resolve, interval)); try { let response = await axios_1.default.post(url, params, config); const authToken = response.data; authToken.expires_at = Math.floor(Date.now() / 1000) + authToken.expires_in; authToken.refresh_expires_at = Math.floor(Date.now() / 1000) + authToken.refresh_expires_in; this.setConfig("authToken", authToken); lib_1.log.debug(`:: EXIT : ${JSON.stringify(authToken)}`); return authToken; } catch (error) { if (attempts % 5 == 0) { lib_1.log.info(`${chalk_1.default.cyanBright(`\nsimba: still waiting for user to login...`)}`); } attempts += 1; } } lib_1.log.debug(`:: EXIT : attempts exceeded, timedout`); return; } else { lib_1.log.debug(`:: entering refresh logic`); const authToken = this.getConfig("authToken"); lib_1.log.debug(`:: auth!! : ${JSON.stringify(authToken)}`); const _refreshToken = authToken.refresh_token; params.append("client_id", this.clientID); params.append("grant_type", "refresh_token"); params.append("refresh_token", _refreshToken); await new Promise(resolve => setTimeout(resolve, interval)); try { let response = await axios_1.default.post(url, params, config); const newAuthToken = response.data; newAuthToken.expires_at = Math.floor(Date.now() / 1000) + newAuthToken.expires_in; newAuthToken.refresh_expires_at = Math.floor(Date.now() / 1000) + newAuthToken.refresh_expires_in; this.setConfig("authToken", newAuthToken); lib_1.log.debug(`:: EXIT : ${JSON.stringify(newAuthToken)}`); return newAuthToken; } catch (error) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${JSON.stringify(error)}`)}`); return; } } } tokenExpired() { lib_1.log.debug(`:: ENTER :`); if (!this.hasConfig("authToken")) { lib_1.log.debug(`:: EXIT : no authToken exists, exiting with true`); return true; } const authToken = this.getConfig("authToken"); if (!authToken.expires_at) { lib_1.log.debug(`:: EXIT : true`); return true; } // return true below, to pad for time required for operations if (authToken.expires_at < Math.floor(Date.now() / 1000) - this.tokenExpirationPad) { lib_1.log.debug(`:: EXIT : access_token expiring within 60 seconds, returning true`); return true; } lib_1.log.debug(`:: EXIT : false`); return false; } refreshTokenExpired() { lib_1.log.debug(`:: ENTER :`); if (!this.hasConfig("authToken")) { lib_1.log.debug(`:: EXIT : true`); return true; } const authToken = this.getConfig("authToken"); if (!authToken.refresh_expires_at) { lib_1.log.debug(`:: EXIT : true`); return true; } // return true below, to pad for time required for operations if (authToken.refresh_expires_at <= Math.floor(Date.now() / 1000) - this.tokenExpirationPad) { lib_1.log.debug(`:: EXIT : access_token expiring within 60 seconds, returning true`); return true; } lib_1.log.debug(`:: EXIT : false`); return false; } async refreshToken() { lib_1.log.debug(`:: ENTER :`); if (!this.tokenExpired()) { lib_1.log.debug(`:: EXIT : authToken still valid`); return; } if (this.refreshTokenExpired()) { this.deleteAuthInfo(); const authToken = await this.loginAndGetAuthToken(); if (authToken) { lib_1.log.debug(`:: EXIT : ${JSON.stringify(authToken)}`); return authToken; } else { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return; } } lib_1.log.debug(`:: entering logic to refresh token`); const pollingConfig = { maxAttempts: 60, interval: 3000, }; const refreshing = true; const newAuthToken = await this.getAuthToken(pollingConfig, refreshing); if (newAuthToken) { lib_1.log.debug(`:: EXIT : ${JSON.stringify(newAuthToken)}`); return newAuthToken; } else { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return; } } async loginAndGetAuthToken(refreshing = false) { lib_1.log.debug(`:: ENTER :`); let verificationCompleteURI; if (!refreshing) { this.logout(); verificationCompleteURI = await this.loginUser(); } if (verificationCompleteURI) { const pollingConfig = { maxAttempts: 60, interval: 3000, }; const authToken = await this.getAuthToken(pollingConfig, refreshing); lib_1.log.debug(`:: EXIT : authToken : ${JSON.stringify(authToken)}`); this.setLoggedInStatus(true); return authToken; } else { lib_1.log.debug(`:: EXIT : ${this.authErrors.verificationInfoError}`); return; } } async accessTokenHeader() { lib_1.log.debug(`:: ENTER :`); let authToken = this.getConfig("authToken"); if (!authToken) { authToken = await this.loginAndGetAuthToken(false); } if (authToken) { const accessToken = authToken.access_token; const headers = { Authorization: `Bearer ${accessToken}`, }; lib_1.log.debug(`:: EXIT : headers: ${JSON.stringify(headers)}`); return headers; } else { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return; } } buildURL(urlExtension) { lib_1.log.debug(`:: ENTER : ${urlExtension}`); let baseURL = this.baseURL.endsWith("/") ? this.baseURL : this.baseURL + "/"; baseURL = baseURL.endsWith("v2/") ? baseURL : baseURL.slice(0, -1) + "v2/"; const fullURL = baseURL + urlExtension; lib_1.log.debug(`:: EXIT : ${fullURL}`); return fullURL; } async doGetRequest(url, contentType, _queryParams, _buildURL = true) { const funcParams = { url, _queryParams, contentType, }; lib_1.log.debug(`:: ENTER : ${JSON.stringify(funcParams)}`); if (this.tokenExpired()) { if (this.refreshTokenExpired()) { lib_1.log.info(`:: INFO : token expired, please log in again`); const authToken = await this.loginAndGetAuthToken(); if (!authToken) { lib_1.log.error(`${chalk_1.default.red(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return new Error(`${this.authErrors.authTokenError}`); } } else { lib_1.log.info(`:: INFO : refreshing token`); const newAuthToken = await this.refreshToken(); if (!newAuthToken) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return new Error(`${this.authErrors.authTokenError}`); } } } const queryParams = _queryParams ? _queryParams : {}; const headers = await this.accessTokenHeader(); if (headers) { if (!contentType) { headers["accept"] = "application/json"; } else { headers["content-type"] = contentType; } const params = new url_1.URLSearchParams(); params.append('client_id', this.clientID); for (const [key, value] of Object.entries(queryParams)) { params.append(key, value); } const config = { headers: headers, }; try { if (_buildURL) { url = this.buildURL(url); } const res = await axios_1.default.get(url, config); const resData = res.data; lib_1.log.debug(`:: EXIT : ${JSON.stringify(resData)}`); return resData; } catch (error) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${JSON.stringify(error)}`)}`); return error; } } else { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return new Error(`${this.authErrors.authTokenError}`); } } async doKeycloakGetRequest(url, _queryParams) { const funcParams = { url, _queryParams, }; lib_1.log.debug(`:: ENTER : ${JSON.stringify(funcParams)}`); const contentType = 'application/x-www-form-urlencoded;charset=utf-8'; const resData = await this.doGetRequest(url, contentType, _queryParams, false); if (resData instanceof Error) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${JSON.stringify(resData)}`)}`); return resData; } lib_1.log.debug(`:: EXIT : result data : ${JSON.stringify(resData)}`); return resData; } async doPostRequest(url, _postData, contentType, _buildURL = true) { const funcParams = { url, _postData, contentType, }; lib_1.log.debug(`:: ENTER : ${JSON.stringify(funcParams)}`); if (this.tokenExpired()) { if (this.refreshTokenExpired()) { lib_1.log.info(`:: INFO : token expired, please log in again`); const authToken = await this.loginAndGetAuthToken(); if (!authToken) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return new Error(`${this.authErrors.authTokenError}`); } } else { lib_1.log.info(`:: INFO : refreshing token`); const newAuthToken = await this.refreshToken(); if (!newAuthToken) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.authTokenError}`)}`); return new Error(`${this.authErrors.authTokenError}`); } } } const postData = _postData ? _postData : {}; const headers = await this.accessTokenHeader(); if (headers) { if (!contentType) { headers["content-type"] = "application/json"; } else { headers["content-type"] = contentType; } const config = { headers: headers, }; try { if (_buildURL) { url = this.buildURL(url); } const res = await axios_1.default.post(url, postData, config); const resData = res.data; lib_1.log.debug(`:: EXIT : ${JSON.stringify(resData)}`); return resData; } catch (error) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${JSON.stringify(error)}`)}`); return error; } } else { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${this.authErrors.headersError}`)}`); return new Error(`${this.authErrors.headersError}`); } } async doKeycloakPostRequest(url, _postData) { const funcParams = { url, _postData, }; lib_1.log.debug(`:: ENTER : ${JSON.stringify(funcParams)}`); const contentType = 'application/x-www-form-urlencoded;charset=utf-8'; const resData = await this.doPostRequest(url, _postData, contentType, false); if (resData instanceof Error) { lib_1.log.error(`${chalk_1.default.redBright(`\nsimba: EXIT : ${JSON.stringify(resData)}`)}`); return resData; } lib_1.log.debug(`:: EXIT : result data : ${JSON.stringify(resData)}`); return resData; } } exports.KeycloakHandler = KeycloakHandler; //# sourceMappingURL=authentication.js.map