@pnp/cli-microsoft365
Version:
Manage Microsoft 365 and SharePoint Framework projects on any platform
832 lines • 40.9 kB
JavaScript
import { AzureCloudInstance } from '@azure/msal-common';
import assert from 'assert';
import { CommandError } from './Command.js';
import { FileTokenStorage } from './auth/FileTokenStorage.js';
import { MsalNetworkClient } from './auth/MsalNetworkClient.js';
import { msalCachePlugin } from './auth/msalCachePlugin.js';
import { cli } from './cli/cli.js';
import request from './request.js';
import { settingsNames } from './settingsNames.js';
import * as accessTokenUtil from './utils/accessToken.js';
import { browserUtil } from './utils/browserUtil.js';
export var CloudType;
(function (CloudType) {
CloudType["Public"] = "Public";
CloudType["USGov"] = "USGov";
CloudType["USGovHigh"] = "USGovHigh";
CloudType["USGovDoD"] = "USGovDoD";
CloudType["China"] = "China";
})(CloudType || (CloudType = {}));
export class Connection {
constructor() {
this.active = false;
this.authType = AuthType.DeviceCode;
this.certificateType = CertificateType.Unknown;
// ID of the tenant where the Microsoft Entra app is registered; common if multi-tenant
this.tenant = 'common';
this.cloudType = CloudType.Public;
this.accessTokens = {};
this.cloudType = CloudType.Public;
}
deactivate() {
this.active = false;
this.name = undefined;
this.identityName = undefined;
this.identityId = undefined;
this.identityTenantId = undefined;
this.accessTokens = {};
this.authType = AuthType.DeviceCode;
this.userName = undefined;
this.password = undefined;
this.certificateType = CertificateType.Unknown;
this.cloudType = CloudType.Public;
this.certificate = undefined;
this.thumbprint = undefined;
this.spoUrl = undefined;
this.spoTenantId = undefined;
this.appId = cli.getClientId();
this.tenant = cli.getTenant();
}
}
export var AuthType;
(function (AuthType) {
AuthType["DeviceCode"] = "deviceCode";
AuthType["Password"] = "password";
AuthType["Certificate"] = "certificate";
AuthType["Identity"] = "identity";
AuthType["FederatedIdentity"] = "federatedIdentity";
AuthType["Browser"] = "browser";
AuthType["Secret"] = "secret";
})(AuthType || (AuthType = {}));
export var CertificateType;
(function (CertificateType) {
CertificateType[CertificateType["Unknown"] = 0] = "Unknown";
CertificateType[CertificateType["Base64"] = 1] = "Base64";
CertificateType[CertificateType["Binary"] = 2] = "Binary";
})(CertificateType || (CertificateType = {}));
export class Auth {
// Retrieves the connections from the file store if it's not already loaded
async getAllConnections() {
if (this._allConnections === undefined) {
try {
this._allConnections = await this.getAllConnectionsFromStorage();
}
catch {
this._allConnections = [];
}
}
return this._allConnections;
}
get connection() {
return this._connection;
}
get defaultResource() {
return Auth.getEndpointForResource('https://graph.microsoft.com', this._connection.cloudType);
}
constructor() {
this._connection = new Connection();
}
// we need to init cloud endpoints here, because we're using CloudType enum
// as indexers, which we can't do in the static initializer
// it also needs to be a separate method that we call here, because in tests
// we're mocking auth and calling its constructor
static initialize() {
this.cloudEndpoints[CloudType.USGov] = {
'https://graph.microsoft.com': 'https://graph.microsoft.com',
'https://management.azure.com/': 'https://management.usgovcloudapi.net/',
'https://login.microsoftonline.com': 'https://login.microsoftonline.com'
};
this.cloudEndpoints[CloudType.USGovHigh] = {
'https://graph.microsoft.com': 'https://graph.microsoft.us',
'https://management.azure.com/': 'https://management.usgovcloudapi.net/',
'https://login.microsoftonline.com': 'https://login.microsoftonline.us'
};
this.cloudEndpoints[CloudType.USGovDoD] = {
'https://graph.microsoft.com': 'https://dod-graph.microsoft.us',
'https://management.azure.com/': 'https://management.usgovcloudapi.net/',
'https://login.microsoftonline.com': 'https://login.microsoftonline.us'
};
this.cloudEndpoints[CloudType.China] = {
'https://graph.microsoft.com': 'https://microsoftgraph.chinacloudapi.cn',
'https://management.azure.com/': 'https://management.chinacloudapi.cn',
'https://login.microsoftonline.com': 'https://login.chinacloudapi.cn'
};
}
async restoreAuth() {
// check if auth has been restored previously
if (this._connection.active) {
return;
}
try {
const connection = await this.getConnectionInfoFromStorage();
this._connection = Object.assign(this._connection, connection);
}
catch {
}
}
async ensureAccessToken(resource, logger, debug = false, fetchNew = false) {
const now = new Date();
const accessToken = this.connection.accessTokens[resource];
const expiresOn = accessToken && accessToken.expiresOn ?
// if expiresOn is serialized from the service file, it's set as a string
// if it's coming from MSAL, it's a Date
typeof accessToken.expiresOn === 'string' ? new Date(accessToken.expiresOn) : accessToken.expiresOn
: new Date(0);
if (!fetchNew && accessToken && expiresOn > now) {
if (debug) {
await logger.logToStderr(`Existing access token ${accessToken.accessToken} still valid. Returning...`);
}
return accessToken.accessToken;
}
else {
if (debug) {
if (!accessToken) {
await logger.logToStderr(`No token found for resource ${resource}.`);
}
else {
await logger.logToStderr(`Access token expired. Token: ${accessToken.accessToken}, ExpiresAt: ${accessToken.expiresOn}`);
}
}
}
let getTokenPromise;
// When using an application identity, you can't retrieve the access token silently, because there is
// no account. Also (for cert auth) clientApplication is instantiated later
// after inspecting the specified cert and calculating thumbprint if one
// wasn't specified
if (this.connection.authType !== AuthType.Certificate &&
this.connection.authType !== AuthType.Secret &&
this.connection.authType !== AuthType.Identity &&
this.connection.authType !== AuthType.FederatedIdentity) {
this.clientApplication = await this.getPublicClient(logger, debug);
if (this.clientApplication) {
const accounts = await this.clientApplication.getTokenCache().getAllAccounts();
// if there is an account in the cache and it's active, we can try to get the token silently
if (accounts.filter(a => a.localAccountId === this.connection.identityId).length > 0 && this.connection.active === true) {
getTokenPromise = this.ensureAccessTokenSilent.bind(this);
}
}
}
if (!getTokenPromise) {
switch (this.connection.authType) {
case AuthType.DeviceCode:
getTokenPromise = this.ensureAccessTokenWithDeviceCode.bind(this);
break;
case AuthType.Password:
getTokenPromise = this.ensureAccessTokenWithPassword.bind(this);
break;
case AuthType.Certificate:
getTokenPromise = this.ensureAccessTokenWithCertificate.bind(this);
break;
case AuthType.Identity:
getTokenPromise = this.ensureAccessTokenWithIdentity.bind(this);
break;
case AuthType.FederatedIdentity:
getTokenPromise = this.ensureAccessTokenWithFederatedIdentity.bind(this);
break;
case AuthType.Browser:
getTokenPromise = this.ensureAccessTokenWithBrowser.bind(this);
break;
case AuthType.Secret:
getTokenPromise = this.ensureAccessTokenWithSecret.bind(this);
break;
}
}
const response = await getTokenPromise(resource, logger, debug, fetchNew);
if (!response) {
if (debug) {
await logger.logToStderr('getTokenPromise authentication result is null.');
}
throw 'Failed to retrieve an access token. Please try again.';
}
else {
if (debug) {
await logger.logToStderr('Response');
await logger.logToStderr(response);
await logger.logToStderr('');
}
}
this.connection.accessTokens[resource] = {
expiresOn: response.expiresOn,
accessToken: response.accessToken
};
this.connection.active = true;
this.connection.identityName = accessTokenUtil.accessToken.getUserNameFromAccessToken(response.accessToken);
this.connection.identityId = accessTokenUtil.accessToken.getUserIdFromAccessToken(response.accessToken);
this.connection.identityTenantId = accessTokenUtil.accessToken.getTenantIdFromAccessToken(response.accessToken);
this.connection.name = this.connection.name || this.connection.identityId;
try {
await this.storeConnectionInfo();
}
catch (ex) {
// error could happen due to an issue with persisting the access
// token which shouldn't fail the overall token retrieval process
if (debug) {
await logger.logToStderr(new CommandError(ex));
}
}
return response.accessToken;
}
async getAuthClientConfiguration(logger, debug, certificateThumbprint, certificatePrivateKey, clientSecret) {
const msal = await import('@azure/msal-node');
const { LogLevel } = msal;
const cert = !certificateThumbprint ? undefined : {
thumbprint: certificateThumbprint,
privateKey: certificatePrivateKey
};
let azureCloudInstance = AzureCloudInstance.None;
switch (this.connection.cloudType) {
case CloudType.Public:
case CloudType.USGov:
azureCloudInstance = AzureCloudInstance.AzurePublic;
break;
case CloudType.China:
azureCloudInstance = AzureCloudInstance.AzureChina;
break;
case CloudType.USGovHigh:
case CloudType.USGovDoD:
azureCloudInstance = AzureCloudInstance.AzureUsGovernment;
break;
}
const config = {
clientId: this.connection.appId,
authority: `${Auth.getEndpointForResource('https://login.microsoftonline.com', this.connection.cloudType)}/${this.connection.tenant}`,
azureCloudOptions: {
azureCloudInstance,
tenant: this.connection.tenant
}
};
const authConfig = cert
? { ...config, clientCertificate: cert }
: { ...config, clientSecret };
return {
auth: authConfig,
cache: {
cachePlugin: msalCachePlugin
},
system: {
loggerOptions: {
// loggerCallback is called by MSAL which we're not testing
/* c8 ignore next 4 */
loggerCallback: async (level, message) => {
if (level === LogLevel.Error || debug) {
await logger.logToStderr(message);
}
},
piiLoggingEnabled: false,
logLevel: debug ? LogLevel.Verbose : LogLevel.Error
},
networkClient: new MsalNetworkClient()
}
};
}
async getPublicClient(logger, debug) {
const msal = await import('@azure/msal-node');
const { PublicClientApplication } = msal;
if (this.connection.authType === AuthType.Password &&
this.connection.tenant === 'common') {
// common is not supported for the password flow and must be changed to
// organizations
this.connection.tenant = 'organizations';
}
return new PublicClientApplication(await this.getAuthClientConfiguration(logger, debug));
}
async getConfidentialClient(logger, debug, certificateThumbprint, certificatePrivateKey, clientSecret) {
const msal = await import('@azure/msal-node');
const { ConfidentialClientApplication } = msal;
return new ConfidentialClientApplication(await this.getAuthClientConfiguration(logger, debug, certificateThumbprint, certificatePrivateKey, clientSecret));
}
retrieveAuthCodeWithBrowser(resource, logger, debug) {
return new Promise(async (resolve, reject) => {
// _authServer is never set before hitting this line, but this check
// is implemented so that we can support lazy loading
// but also stub it for testing
/* c8 ignore next 3 */
if (!this._authServer) {
this._authServer = (await import('./AuthServer.js')).default;
}
this._authServer.initializeServer(this.connection, resource, resolve, reject, logger, debug);
});
}
async ensureAccessTokenWithBrowser(resource, logger, debug) {
if (debug) {
await logger.logToStderr(`Retrieving new access token using interactive browser session...`);
}
const response = await this.retrieveAuthCodeWithBrowser(resource, logger, debug);
if (debug) {
await logger.logToStderr(`The service returned the code '${response.code}'`);
}
return this.clientApplication.acquireTokenByCode({
code: response.code,
redirectUri: response.redirectUri,
scopes: [`${resource}/.default`]
});
}
async ensureAccessTokenSilent(resource, logger, debug, fetchNew) {
if (debug) {
await logger.logToStderr(`Retrieving new access token silently`);
}
// Asserting identityId because it is expected to be available at this point.
assert(this.connection.identityId !== undefined);
const account = await this.clientApplication
.getTokenCache().getAccountByLocalId(this.connection.identityId);
// Asserting account because it is expected to be available at this point.
assert(account !== null);
return this.clientApplication.acquireTokenSilent({
account: account,
scopes: [`${resource}/.default`],
forceRefresh: fetchNew
});
}
async ensureAccessTokenWithDeviceCode(resource, logger, debug) {
if (debug) {
await logger.logToStderr(`Starting Auth.ensureAccessTokenWithDeviceCode. resource: ${resource}, debug: ${debug}`);
}
this.deviceCodeRequest = {
// deviceCodeCallback is called by MSAL which we're not testing
/* c8 ignore next 1 */
deviceCodeCallback: response => this.processDeviceCodeCallback(response, logger, debug),
scopes: [`${resource}/.default`]
};
return this.clientApplication.acquireTokenByDeviceCode(this.deviceCodeRequest);
}
async processDeviceCodeCallback(response, logger, debug) {
if (debug) {
await logger.logToStderr('Response:');
await logger.logToStderr(response);
await logger.logToStderr('');
}
if (response.message) {
await logger.logToStderr(`🌶️ ${response.message}`);
}
if (cli.getSettingWithDefaultValue(settingsNames.autoOpenLinksInBrowser, false)) {
await browserUtil.open(response.verificationUri);
}
if (cli.getSettingWithDefaultValue(settingsNames.copyDeviceCodeToClipboard, false)) {
// _clipboardy is never set before hitting this line, but this check
// is implemented so that we can support lazy loading
// but also stub it for testing
/* c8 ignore next 3 */
if (!this._clipboardy) {
this._clipboardy = (await import('clipboardy')).default;
}
this._clipboardy.writeSync(response.userCode);
}
}
async ensureAccessTokenWithPassword(resource, logger, debug) {
if (debug) {
await logger.logToStderr(`Retrieving new access token using credentials...`);
}
return this.clientApplication.acquireTokenByUsernamePassword({
username: this.connection.userName,
password: this.connection.password,
scopes: [`${resource}/.default`]
});
}
async ensureAccessTokenWithCertificate(resource, logger, debug, fetchNew) {
const nodeForge = (await import('node-forge')).default;
const { pem, pki, asn1, pkcs12 } = nodeForge;
if (debug) {
await logger.logToStderr(`Retrieving new access token using certificate...`);
}
let cert = '';
const buf = Buffer.from(this.connection.certificate, 'base64');
if (this.connection.certificateType === CertificateType.Unknown || this.connection.certificateType === CertificateType.Base64) {
// First time this method is called, we don't know if certificate is PEM or PFX (type is Unknown)
// We assume it is PEM but when parsing of PEM fails, we assume it could be PFX
// Type is persisted on service so subsequent calls only run through the correct parsing flow
try {
cert = buf.toString('utf8');
const pemObjs = pem.decode(cert);
if (this.connection.thumbprint === undefined) {
const pemCertObj = pemObjs.find(pem => pem.type === "CERTIFICATE");
const pemCertStr = pem.encode(pemCertObj);
const pemCert = pki.certificateFromPem(pemCertStr);
this.connection.thumbprint = await this.calculateThumbprint(pemCert);
}
}
catch (e) {
this.connection.certificateType = CertificateType.Binary;
}
}
if (this.connection.certificateType === CertificateType.Binary) {
const p12Asn1 = asn1.fromDer(buf.toString('binary'), false);
const p12Parsed = pkcs12.pkcs12FromAsn1(p12Asn1, false, this.connection.password);
let keyBags = p12Parsed.getBags({ bagType: pki.oids.pkcs8ShroudedKeyBag });
const pkcs8ShroudedKeyBag = keyBags[pki.oids.pkcs8ShroudedKeyBag][0];
if (debug) {
// check if there is something in the keyBag as well as
// the pkcs8ShroudedKeyBag. This will give us more information
// whether there is a cert that can potentially store keys in the keyBag.
// I could not find a way to add something to the keyBag with all
// my attempts, but lets keep it here for troubleshooting purposes.
await logger.logToStderr(`pkcs8ShroudedKeyBagkeyBags length is ${[pki.oids.pkcs8ShroudedKeyBag].length}`);
keyBags = p12Parsed.getBags({ bagType: pki.oids.keyBag });
await logger.logToStderr(`keyBag length is ${keyBags[pki.oids.keyBag].length}`);
}
// convert a Forge private key to an ASN.1 RSAPrivateKey
const rsaPrivateKey = pki.privateKeyToAsn1(pkcs8ShroudedKeyBag.key);
// wrap an RSAPrivateKey ASN.1 object in a PKCS#8 ASN.1 PrivateKeyInfo
const privateKeyInfo = pki.wrapRsaPrivateKey(rsaPrivateKey);
// convert a PKCS#8 ASN.1 PrivateKeyInfo to PEM
cert = pki.privateKeyInfoToPem(privateKeyInfo);
if (this.connection.thumbprint === undefined) {
const certBags = p12Parsed.getBags({ bagType: pki.oids.certBag });
const certBag = (certBags[pki.oids.certBag])[0];
this.connection.thumbprint = await this.calculateThumbprint(certBag.cert);
}
}
this.clientApplication = await this.getConfidentialClient(logger, debug, this.connection.thumbprint, cert);
return this.clientApplication.acquireTokenByClientCredential({
scopes: [`${resource}/.default`],
skipCache: fetchNew
});
}
async ensureAccessTokenWithIdentity(resource, logger, debug) {
const userName = this.connection.userName;
if (debug) {
await logger.logToStderr('Will try to retrieve access token using identity...');
}
const requestOptions = {
url: '',
headers: {
accept: 'application/json',
Metadata: true,
'x-anonymous': true
},
responseType: 'json'
};
if (process.env.IDENTITY_ENDPOINT && process.env.IDENTITY_HEADER) {
if (debug) {
await logger.logToStderr('IDENTITY_ENDPOINT and IDENTITY_HEADER env variables found it is Azure Function, WebApp...');
}
requestOptions.url = `${process.env.IDENTITY_ENDPOINT}?resource=${encodeURIComponent(resource)}&api-version=2019-08-01`;
requestOptions.headers['X-IDENTITY-HEADER'] = process.env.IDENTITY_HEADER;
}
else if (process.env.MSI_ENDPOINT && process.env.MSI_SECRET) {
if (debug) {
await logger.logToStderr('MSI_ENDPOINT and MSI_SECRET env variables found it is Azure Function or WebApp, but using the old names of the env variables...');
}
requestOptions.url = `${process.env.MSI_ENDPOINT}?resource=${encodeURIComponent(resource)}&api-version=2019-08-01`;
requestOptions.headers['X-IDENTITY-HEADER'] = process.env.MSI_SECRET;
}
else if (process.env.IDENTITY_ENDPOINT) {
if (debug) {
await logger.logToStderr('IDENTITY_ENDPOINT env variable found it is Azure Could Shell...');
}
if (userName && process.env.ACC_CLOUD) {
// reject for now since the Azure Cloud Shell does not support user-managed identity
throw 'Azure Cloud Shell does not support user-managed identity. You can execute the command without the --userName option to login with user identity';
}
requestOptions.url = `${process.env.IDENTITY_ENDPOINT}?resource=${encodeURIComponent(resource)}`;
}
else if (process.env.MSI_ENDPOINT) {
if (debug) {
await logger.logToStderr('MSI_ENDPOINT env variable found it is Azure Could Shell, but using the old names of the env variables...');
}
if (userName && process.env.ACC_CLOUD) {
// reject for now since the Azure Cloud Shell does not support user-managed identity
throw 'Azure Cloud Shell does not support user-managed identity. You can execute the command without the --userName option to login with user identity';
}
requestOptions.url = `${process.env.MSI_ENDPOINT}?resource=${encodeURIComponent(resource)}`;
}
else {
if (debug) {
await logger.logToStderr('IDENTITY_ENDPOINT and MSI_ENDPOINT env variables not found. Attempt to get Managed Identity token by using the Azure Virtual Machine API...');
}
requestOptions.url = `http://169.254.169.254/metadata/identity/oauth2/token?resource=${encodeURIComponent(resource)}&api-version=2018-02-01`;
}
if (userName) {
// if name present then the identity is user-assigned managed identity
// the name option in this case is either client_id or principal_id (object_id)
// of the managed identity service principal
requestOptions.url += `&client_id=${encodeURIComponent(userName)}`;
if (debug) {
await logger.logToStderr('Wil try to get token using client_id param...');
}
}
try {
const accessTokenResponse = await request.get(requestOptions);
return {
accessToken: accessTokenResponse.access_token,
expiresOn: new Date(parseInt(accessTokenResponse.expires_on) * 1000)
};
}
catch (e) {
if (!userName) {
throw e;
}
// since the userName option can be either client_id or principal_id (object_id)
// and the first attempt was using client_id
// now lets see if the api returned 'not found' response and
// try to get token using principal_id (object_id)
let isNotFoundResponse = false;
if (e.error && e.error.Message) {
// check if it is Azure Function api 'not found' response
isNotFoundResponse = (e.error.Message.indexOf("No Managed Identity found") !== -1);
}
else if (e.error && e.error.error_description) {
// check if it is Azure VM api 'not found' response
isNotFoundResponse = (e.error.error_description === "Identity not found");
}
if (!isNotFoundResponse) {
// it is not a 'not found' response then exit with error
throw e;
}
if (debug) {
await logger.logToStderr('Wil try to get token using principal_id (also known as object_id) param ...');
}
requestOptions.url = requestOptions.url.replace('&client_id=', '&principal_id=');
requestOptions.headers['x-anonymous'] = true;
try {
const accessTokenResponse = await request.get(requestOptions);
return {
accessToken: accessTokenResponse.access_token,
expiresOn: new Date(parseInt(accessTokenResponse.expires_on) * 1000)
};
}
catch (err) {
// will give up and not try any further with the 'msi_res_id' (resource id) query string param
// since it does not work with the Azure Functions api, but just with the Azure VM api
if (err.error.code === 'EACCES') {
// the CLI does not know if managed identity is actually assigned when EACCES code thrown
// so show meaningful message since the raw error response could be misleading
return Promise.reject('Error while logging with Managed Identity. Please check if a Managed Identity is assigned to the current Azure resource.');
}
else {
throw err;
}
}
}
}
async ensureAccessTokenWithFederatedIdentity(resource, logger, debug) {
if (debug) {
await logger.logToStderr('Trying to retrieve access token using federated identity...');
}
if (process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN) {
if (debug) {
await logger.logToStderr('ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN env variables found. The context is GitHub Actions...');
}
const federationToken = await this.getFederationTokenFromGithub(logger, debug);
return this.getAccessTokenWithFederatedToken(resource, federationToken, logger, debug);
}
else if (process.env.SYSTEM_OIDCREQUESTURI) {
if (debug) {
await logger.logToStderr('SYSTEM_OIDCREQUESTURI env variable found. The context is Azure DevOps...');
}
if (!process.env.SYSTEM_ACCESSTOKEN) {
throw new CommandError(`The SYSTEM_ACCESSTOKEN environment variable is not available. Please check the Azure DevOps pipeline task configuration. It should contain 'SYSTEM_ACCESSTOKEN: $(System.AccessToken)' in the env section.`);
}
const serviceConnectionId = process.env.AZURESUBSCRIPTION_SERVICE_CONNECTION_ID;
const serviceConnectionAppId = process.env.AZURESUBSCRIPTION_CLIENT_ID;
const serviceConnectionTenantId = process.env.AZURESUBSCRIPTION_TENANT_ID;
const useServiceConnection = serviceConnectionId && serviceConnectionAppId && serviceConnectionTenantId;
if (!useServiceConnection) {
if (debug) {
await logger.logToStderr('Not using a service connection. Run this command in an AzurePowerShell task to be able to use a service connection.');
}
if (!this.connection.appId || this.connection.tenant === 'common') {
throw new CommandError('The appId and tenant parameters are required when not using a service connection.');
}
}
else {
if (debug) {
if (this.connection.appId || this.connection.tenant !== 'common') {
await logger.logToStderr('When using a service connection, the appId and tenant values are updated to the values of the service connection.');
}
await logger.logToStderr(`Using service connection '${serviceConnectionId}' with app Id '${serviceConnectionAppId}' and tenant Id '${serviceConnectionTenantId}'...`);
}
this.connection.appId = serviceConnectionAppId;
this.connection.tenant = serviceConnectionTenantId;
}
const federationToken = await this.getFederationTokenFromAzureDevOps(logger, debug, serviceConnectionId);
return this.getAccessTokenWithFederatedToken(resource, federationToken, logger, debug);
}
else {
throw new CommandError('Federated identity is currently only supported in GitHub Actions and Azure DevOps.');
}
}
async getFederationTokenFromGithub(logger, debug) {
if (debug) {
await logger.logToStderr('Retrieving GitHub federation token...');
}
const requestOptions = {
url: `${process.env.ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${encodeURIComponent('api://AzureADTokenExchange')}`,
headers: {
Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
Accept: 'application/json',
'x-anonymous': true
},
responseType: 'json'
};
const accessTokenResponse = await request.get(requestOptions);
return accessTokenResponse.value;
}
async getFederationTokenFromAzureDevOps(logger, debug, serviceConnectionId) {
if (debug) {
await logger.logToStderr('Retrieving Azure DevOps federation token...');
}
const urlSuffix = serviceConnectionId ? `&serviceConnectionId=${serviceConnectionId}` : '';
const requestOptions = {
url: `${process.env.SYSTEM_OIDCREQUESTURI}?api-version=7.1${urlSuffix}`,
headers: {
Authorization: `Bearer ${process.env.SYSTEM_ACCESSTOKEN}`,
Accept: 'application/json',
'Content-Type': 'application/json',
'x-anonymous': true
},
responseType: 'json'
};
const accessTokenResponse = await request.post(requestOptions);
return accessTokenResponse.oidcToken;
}
async getAccessTokenWithFederatedToken(resource, federatedToken, logger, debug) {
if (debug) {
await logger.logToStderr('Retrieving Entra ID Access Token with federated token...');
}
const queryParams = [
'grant_type=client_credentials',
`scope=${encodeURIComponent(`${resource}/.default`)}`,
`client_id=${this.connection.appId}`,
`client_assertion_type=${encodeURIComponent('urn:ietf:params:oauth:client-assertion-type:jwt-bearer')}`,
`client_assertion=${federatedToken}`
];
const requestOptions = {
url: `https://login.microsoftonline.com/${this.connection.tenant}/oauth2/v2.0/token`,
headers: {
accept: 'application/json',
'x-anonymous': true,
'Content-Type': 'application/x-www-form-urlencoded'
},
data: queryParams.join('&'),
responseType: 'json'
};
const accessTokenResponse = await request.post(requestOptions);
const expiresIn = parseInt(accessTokenResponse.expires_in) * 1000;
const now = new Date();
return {
accessToken: accessTokenResponse.access_token,
expiresOn: new Date(now.getTime() + expiresIn)
};
}
async ensureAccessTokenWithSecret(resource, logger, debug, fetchNew) {
this.clientApplication = await this.getConfidentialClient(logger, debug, undefined, undefined, this.connection.secret);
return this.clientApplication.acquireTokenByClientCredential({
scopes: [`${resource}/.default`],
skipCache: fetchNew
});
}
async calculateThumbprint(certificate) {
const nodeForge = (await import('node-forge')).default;
const { md, asn1, pki } = nodeForge;
const messageDigest = md.sha1.create();
messageDigest.update(asn1.toDer(pki.certificateToAsn1(certificate)).getBytes());
return messageDigest.digest().toHex();
}
static getResourceFromUrl(url) {
let resource = url;
const pos = resource.indexOf('/', 8);
if (pos > -1) {
resource = resource.substring(0, pos);
}
if (resource === 'https://api.bap.microsoft.com' ||
resource === 'https://api.powerapps.com' ||
resource.endsWith('.api.bap.microsoft.com')) {
resource = 'https://service.powerapps.com/';
}
if (resource === 'https://api.flow.microsoft.com') {
resource = 'https://management.azure.com/';
}
if (resource === 'https://api.powerbi.com') {
// api.powerbi.com is not a valid resource
// we need to use https://analysis.windows.net/powerbi/api instead
resource = 'https://analysis.windows.net/powerbi/api';
}
return resource;
}
async getConnectionInfoFromStorage() {
const tokenStorage = this.getConnectionStorage();
const json = await tokenStorage.get();
return JSON.parse(json);
}
async storeConnectionInfo() {
const connectionStorage = this.getConnectionStorage();
await connectionStorage.set(JSON.stringify(this.connection));
let allConnections = await this.getAllConnections();
if (this.connection.active) {
allConnections = allConnections.filter(c => c.identityId !== this.connection.identityId ||
c.appId !== this.connection.appId ||
c.tenant !== this.connection.tenant);
allConnections.forEach(c => c.active = false);
allConnections = [{ ...this.connection }, ...allConnections];
}
this._allConnections = allConnections;
const allConnectionsStorage = this.getAllConnectionsStorage();
await allConnectionsStorage.set(JSON.stringify(allConnections));
}
async clearConnectionInfo() {
const connectionStorage = this.getConnectionStorage();
const allConnectionsStorage = this.getAllConnectionsStorage();
await connectionStorage.remove();
await allConnectionsStorage.remove();
// we need to manually clear MSAL cache, because MSAL doesn't have support
// for logging out when using cert-based auth
const msalCache = this.getMsalCacheStorage();
await msalCache.remove();
}
async removeConnectionInfo(connection, logger, debug) {
const allConnections = await this.getAllConnections();
const isCurrentConnection = this.connection.name === connection.name;
this._allConnections = allConnections.filter(c => c.name !== connection.name);
// Asserting identityId because it is optional, but required at this point.
assert(connection.identityId !== undefined);
// When using an application identity, there is no account in the MSAL TokenCache
if (this.connection.authType !== AuthType.Certificate &&
this.connection.authType !== AuthType.Secret &&
this.connection.authType !== AuthType.Identity &&
this.connection.authType !== AuthType.FederatedIdentity) {
this.clientApplication = await this.getPublicClient(logger, debug);
if (this.clientApplication) {
const tokenCache = this.clientApplication.getTokenCache();
const account = await tokenCache.getAccountByLocalId(connection.identityId);
if (account !== null) {
await tokenCache.removeAccount(account);
}
}
}
const connectionStorage = this.getConnectionStorage();
const allConnectionsStorage = this.getAllConnectionsStorage();
await allConnectionsStorage.set(JSON.stringify(this._allConnections));
if (isCurrentConnection) {
await connectionStorage.remove();
this.connection.deactivate();
}
}
getConnectionStorage() {
return new FileTokenStorage(FileTokenStorage.connectionInfoFilePath());
}
getMsalCacheStorage() {
return new FileTokenStorage(FileTokenStorage.msalCacheFilePath());
}
getAllConnectionsStorage() {
return new FileTokenStorage(FileTokenStorage.allConnectionsFilePath());
}
static getEndpointForResource(resource, cloudType) {
if (Auth.cloudEndpoints[cloudType] &&
Auth.cloudEndpoints[cloudType][resource]) {
return Auth.cloudEndpoints[cloudType][resource];
}
else {
return resource;
}
}
async getAllConnectionsFromStorage() {
const connectionStorage = this.getAllConnectionsStorage();
const json = await connectionStorage.get();
return JSON.parse(json);
}
async switchToConnection(connection) {
this.connection.deactivate();
this._connection = Object.assign(this._connection, connection);
this._connection.active = true;
await this.storeConnectionInfo();
}
async updateConnection(connection, newName) {
const allConnections = await this.getAllConnections();
const existingConnection = allConnections.find(c => c.name === newName);
const oldName = connection.name;
if (existingConnection) {
throw new CommandError(`The connection name '${newName}' is already in use`);
}
connection.name = newName;
if (this.connection.name === oldName) {
this._connection.name = newName;
}
await this.storeConnectionInfo();
}
async getConnection(name) {
const allConnections = await this.getAllConnections();
const connection = allConnections.find(i => i.name === name);
if (!connection) {
throw new CommandError(`The connection '${name}' cannot be found.`);
}
return connection;
}
getConnectionDetails(connection) {
// Asserting name and identityId because they are optional, but required at this point.
assert(connection.identityName !== undefined);
assert(connection.name !== undefined);
const details = {
connectionName: connection.name,
connectedAs: connection.identityName,
authType: connection.authType,
appId: connection.appId,
appTenant: connection.tenant,
cloudType: CloudType[connection.cloudType]
};
return details;
}
}
Auth.cloudEndpoints = {};
Auth.initialize();
export default new Auth();
//# sourceMappingURL=Auth.js.map