@simbachain/hardhat
Version:
Simba Chain plugin for hardhat
505 lines • 21.4 kB
JavaScript
"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