@adinsure-ops/ops-cli
Version:
Operations CLI for working with AdInsure
308 lines (307 loc) • 13 kB
JavaScript
import { __awaiter } from "tslib";
import { AuthError, ConfidentialClientApplication, CryptoProvider, InteractionRequiredAuthError, PublicClientApplication, } from '@azure/msal-node';
import { reject } from 'lodash-es';
import path from 'path';
import LocalCachePlugin from './cache.js';
/**
* Security context that keeps track of current authentication token.
* It's not recommended you instantiate this class on your own but rather inherit your commands from [[CommandBase]].
*/
export default class SecurityContext {
/**
* Initializes [[SecurityContext]] with config directory where access token JSON will be stored.
* @param configDir Directory where CLI configuration is stored
*/
constructor(configDir) {
this.clientId = 'bed40d09-53d7-4af8-bf63-3288658b65fb';
this.devopsResourceId = '499b84ac-1321-427f-aa17-267ca6975798';
this.tenant = 'adfintech.onmicrosoft.com';
this.authorityUrl = `https://login.microsoftonline.com/${this.tenant}/`;
this.redirectUri = 'http://localhost:3044';
this.resource = 'https://ops.adinsure.com';
this._verifier = null;
this._challenge = null;
this._accessTokenPath = path.resolve(configDir, 'accessToken.json');
this._cryptoProvider = new CryptoProvider();
this._pca = new PublicClientApplication({
auth: {
clientId: this.clientId,
authority: this.authorityUrl,
},
cache: {
cachePlugin: new LocalCachePlugin(this._accessTokenPath),
},
});
}
/**
* Get the accessTokenPath, as it is a private variable
* @returns {string} Returns access token path
*/
getTokenPath() {
return this._accessTokenPath;
}
/**
* Contacts authority server, fetches and saves the authentication token to be used in subsequent requests.
*
* @param code Authorization code used to fetch the token.
* @returns [[Promise<TokenResponse>]]
*/
getLoginUrl() {
return new Promise((resolve) => {
// Generate PKCE Codes before starting the authorization flow
this._cryptoProvider
.generatePkceCodes()
.then(({ verifier, challenge }) => {
this._verifier = verifier;
this._challenge = challenge;
const authCodeUrlParameters = {
scopes: [this.resource + '/.default', 'offline_access'],
redirectUri: this.redirectUri,
codeChallenge: this._challenge,
codeChallengeMethod: 'S256',
};
// Get url to sign user in and consent to scopes needed for applicatio
this._pca
.getAuthCodeUrl(authCodeUrlParameters)
.then((url) => {
resolve(url);
})
.catch((error) => {
reject(JSON.stringify(error));
});
})
.catch((error) => {
reject(error);
});
});
}
/**
* Retrieves the refresh token from the MSAL (Microsoft Authentication Library) token cache.
*
* @returns {string} The refresh token.
*
* @example
* const refreshToken = getRefreshToken();
* console.log(refreshToken);
*
* @description
* The `getRefreshToken` function extracts the refresh token from the serialized token cache
* managed by the MSAL library. This function first serializes the token cache, parses it into
* a JSON object, and then retrieves the refresh token from the parsed object.
*
* @throws {Error} If there are issues with parsing the token cache or accessing the refresh token.
*/
getRefreshToken() {
const tokenCache = this._pca.getTokenCache().serialize();
const refreshTokenObject = JSON.parse(tokenCache).RefreshToken;
const refreshToken = refreshTokenObject[Object.keys(refreshTokenObject)[0]].secret;
return refreshToken;
}
/**
* Gets the current authentication token.
* This function will also automatically try to refresh token if expired.
*
* @throws Error when authentication failed and/or call to login is needed
* @returns Promise that resolves bearer authentication token
*/
getToken() {
return __awaiter(this, void 0, void 0, function* () {
const msalTokenCache = this._pca.getTokenCache();
const cachedAccounts = yield msalTokenCache.getAllAccounts();
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
if (cachedAccounts.length > 0) {
this._pca
.acquireTokenSilent({
account: cachedAccounts[0],
scopes: [this.resource + '/.default', 'offline_access'],
})
.then((response) => {
if (response) {
const { accessToken } = response;
resolve(accessToken);
}
else {
reject(new Error('Error getting token, please call `ops login`.'));
}
})
.catch((error) => __awaiter(this, void 0, void 0, function* () {
if (error instanceof InteractionRequiredAuthError) {
try {
const tokenRespone = yield this._pca.acquireTokenByRefreshToken({
refreshToken: this.getRefreshToken(),
scopes: [this.resource + '/.default'],
forceCache: true,
});
if (tokenRespone === null) {
throw new AuthError('Cannot retrieve user cache and use the refresh token!');
}
resolve(tokenRespone.accessToken);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
}
catch (error) {
reject(new Error(error));
}
}
reject(new Error(error));
}));
}
else if (process.env.AZURE_CLIENT_ID &&
process.env.AZURE_CLIENT_SECRET) {
const cca = new ConfidentialClientApplication({
auth: {
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
authority: this.authorityUrl,
},
cache: {
cachePlugin: new LocalCachePlugin(this._accessTokenPath),
},
});
const msalCcaTokenCache = cca.getTokenCache();
const ccaCachedAccounts = yield msalCcaTokenCache.getAllAccounts();
if (ccaCachedAccounts.length > 0) {
cca.acquireTokenSilent({
account: ccaCachedAccounts[0],
scopes: [this.resource + '/.default'],
})
.then((response) => {
if (response) {
const { accessToken } = response;
resolve(accessToken);
}
else {
reject(new Error('Error getting token, please call `ops login`.'));
}
})
.catch((error) => {
reject(new Error(error));
});
}
else {
cca.acquireTokenByClientCredential({
scopes: [this.resource + '/.default', 'offline_access'],
})
.then((response) => {
if (response) {
const { accessToken } = response;
console.log(accessToken);
resolve(accessToken);
}
else {
reject(new Error('Error getting token, please call `ops login`.'));
}
})
.catch((error) => {
reject(new Error(error));
});
}
}
else {
throw new Error('You are not authenticated, please call `ops login`.');
}
}));
});
}
/**
* Contacts authority server, fetches and saves the authentication token to be used in subsequent requests.
*
* @param query Authorization query used to fetch the token.
* @returns [[Promise<TokenResponse>]]
*/
acquireToken(query) {
return new Promise((resolve) => {
if (this._verifier === null) {
throw new Error(`PKCE verifier missing.`);
}
// Add PKCE code verifier to token request object
const tokenRequest = {
code: query.code,
scopes: [this.resource + '/.default', 'offline_access'],
redirectUri: this.redirectUri,
codeVerifier: this._verifier,
clientInfo: query.client_info,
};
this._pca
.acquireTokenByCode(tokenRequest)
.then((token) => {
var _a;
if ((_a = token.account) === null || _a === void 0 ? void 0 : _a.name) {
resolve(token.account.name);
}
})
.catch((error) => {
if (error instanceof AuthError) {
const authError = error;
console.log(`Error name: + ${authError.name}`);
console.log(`Error message: + ${authError.message}`);
console.log(`Inner error message: + ${authError.errorMessage}`);
console.log(`Error correlation id: + ${authError.correlationId}`);
console.error();
}
throw new Error(error);
});
});
}
/**
* Acquires token using device flow.
*
* @returns [[Promise<TokenResponse>]]
*/
acquireTokenDeviceFlow() {
return new Promise((resolve, reject) => {
this._pca
.acquireTokenByDeviceCode({
deviceCodeCallback(response) {
if (response.message) {
// tslint:disable-next-line: no-console
console.log(response.message);
}
},
scopes: [this.resource + '/.default', 'offline_access'],
})
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
});
}
/**
* Get NPM token for adinsure npm repositories
*
* @returns [[Promise<string>]]
*/
getNPMToken() {
return __awaiter(this, void 0, void 0, function* () {
const msalTokenCache = this._pca.getTokenCache();
const cachedAccounts = yield msalTokenCache.getAllAccounts();
return new Promise((resolve, reject) => {
if (cachedAccounts.length > 0) {
this._pca
.acquireTokenSilent({
account: cachedAccounts[0],
scopes: [
this.devopsResourceId + '/.default',
'offline_access',
],
})
.then((response) => {
if (response) {
resolve(response.accessToken);
}
else {
reject(new Error('Error getting token, please call `ops login`.'));
}
})
.catch((error) => {
reject(new Error(error));
});
}
else {
reject();
}
});
});
}
}