| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- // Copyright (c) Microsoft Corporation. All rights reserved.
- // Licensed under the MIT License. See License.txt in the project root for license information.
- import { Constants as MSRestConstants, WebResource } from "@azure/ms-rest-js";
- import { TokenClientCredentials, TokenResponse } from "./tokenClientCredentials";
- import { LinkedSubscription } from "../subscriptionManagement/subscriptionUtils";
- import { execAz } from "../login";
- interface ParsedToken {
- /**
- * The token audience or the resource.
- */
- aud: string;
- [prop: string]: any;
- }
- /**
- * Describes the access token retrieved from Azure CLI.
- */
- export interface CliAccessToken {
- /**
- * The access token for the resource
- */
- accessToken: string;
- /**
- * Time when the access token expires.
- */
- expiresOn: Date;
- /**
- * SubscriptionId associated with the token.
- */
- subscription: string;
- /**
- * tenantId associated with the token.
- */
- tenant: string;
- /**
- * The token type. example: "Bearer".
- */
- tokenType: string;
- }
- /**
- * Describes the options that can be provided while listing all the subscriptions/accounts via
- * Azure CLI.
- */
- export interface ListAllSubscriptionOptions {
- /**
- * List all subscriptions, rather just 'Enabled' ones.
- */
- all?: boolean;
- /**
- * Retrieve up-to-date subscriptions from server.
- */
- refresh?: boolean;
- }
- export interface AccessTokenOptions {
- /**
- * The subscription id or name for which the access token is required.
- */
- subscriptionIdOrName?: string;
- /**
- * Azure resource endpoints.
- * - Defaults to Azure Resource Manager from environment: AzureCloud. "https://management.azure.com"
- * - For Azure KeyVault: "https://vault.azure.net"
- * - For Azure Batch: "https://batch.core.windows.net"
- * - For Azure Active Directory Graph: "https://graph.windows.net"
- *
- * To get the resource for other clouds:
- * - `az cloud list`
- */
- resource?: string;
- }
- /**
- * Describes the credentials by retrieving token via Azure CLI.
- */
- export class AzureCliCredentials implements TokenClientCredentials {
- /**
- * Provides information about the default/current subscription for Azure CLI.
- */
- subscriptionInfo: LinkedSubscription;
- /**
- * Provides information about the access token for the corresponding subscription for Azure CLI.
- */
- tokenInfo: CliAccessToken;
- /**
- * Azure resource endpoints.
- * - Defaults to Azure Resource Manager from environment: AzureCloud. "https://management.azure.com"
- * - For Azure KeyVault: "https://vault.azure.net"
- * - For Azure Batch: "https://batch.core.windows.net"
- * - For Azure Active Directory Graph: "https://graph.windows.net"
- *
- * To get the resource for other clouds:
- * - `az cloud list`
- */
- // tslint:disable-next-line: no-inferrable-types
- resource: string = "https://management.azure.com";
- /**
- * The number of seconds within which it is good to renew the token.
- * A constant set to 270 seconds (4.5 minutes).
- */
- private readonly _tokenRenewalMarginInSeconds: number = 270;
- constructor(
- subscriptionInfo: LinkedSubscription,
- tokenInfo: CliAccessToken,
- // tslint:disable-next-line: no-inferrable-types
- resource: string = "https://management.azure.com") {
- this.subscriptionInfo = subscriptionInfo;
- this.tokenInfo = tokenInfo;
- this.resource = resource;
- }
- /**
- * Tries to get the new token from Azure CLI, if the token has expired or the subscription has
- * changed else uses the cached accessToken.
- * @return The tokenResponse (tokenType and accessToken are the two important properties).
- */
- public async getToken(): Promise<TokenResponse> {
- if (this._hasTokenExpired() || this._hasSubscriptionChanged() || this._hasResourceChanged()) {
- try {
- // refresh the access token
- this.tokenInfo = await AzureCliCredentials.getAccessToken(
- {
- subscriptionIdOrName: this.subscriptionInfo.id,
- resource: this.resource
- }
- );
- } catch (err) {
- throw new Error(
- `An error occurred while refreshing the new access ` +
- `token:${err.stderr ? err.stderr : err.message}`
- );
- }
- }
- const result: TokenResponse = {
- accessToken: this.tokenInfo.accessToken,
- tokenType: this.tokenInfo.tokenType,
- expiresOn: this.tokenInfo.expiresOn,
- tenantId: this.tokenInfo.tenant
- };
- return result;
- }
- /**
- * Signs a request with the Authentication header.
- * @param The request to be signed.
- */
- public async signRequest(webResource: WebResource): Promise<WebResource> {
- const tokenResponse = await this.getToken();
- webResource.headers.set(
- MSRestConstants.HeaderConstants.AUTHORIZATION,
- `${tokenResponse.tokenType} ${tokenResponse.accessToken}`
- );
- return Promise.resolve(webResource);
- }
- private _hasTokenExpired(): boolean {
- let result = true;
- const now = Math.floor(Date.now() / 1000);
- if (this.tokenInfo.expiresOn &&
- this.tokenInfo.expiresOn instanceof Date &&
- Math.floor(this.tokenInfo.expiresOn.getTime() / 1000) - now > this._tokenRenewalMarginInSeconds) {
- result = false;
- }
- return result;
- }
- private _hasSubscriptionChanged(): boolean {
- return this.subscriptionInfo.id !== this.tokenInfo.subscription;
- }
- private _parseToken(): ParsedToken {
- try {
- const base64Url: string = this.tokenInfo.accessToken.split(".")[1];
- const base64: string = decodeURIComponent(
- Buffer.from(base64Url, "base64").toString("binary").split("").map((c) => {
- return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
- }).join(""));
- return JSON.parse(base64);
- } catch (err) {
- const msg = `An error occurred while parsing the access token: ${err.stack}`;
- throw new Error(msg);
- }
- }
- private _isAzureResourceManagerEndpoint(newResource: string, currentResource: string): boolean {
- if (newResource.endsWith("/")) newResource = newResource.slice(0, -1);
- if (currentResource.endsWith("/")) currentResource = currentResource.slice(0, -1);
- return (newResource === "https://management.core.windows.net" &&
- currentResource === "https://management.azure.com") ||
- (newResource === "https://management.azure.com" &&
- currentResource === "https://management.core.windows.net");
- }
- private _hasResourceChanged(): boolean {
- const parsedToken: ParsedToken = this._parseToken();
- // normalize the resource string, since it is possible to
- // provide a resource without a trailing slash
- const currentResource = parsedToken.aud && parsedToken.aud.endsWith("/")
- ? parsedToken.aud.slice(0, -1)
- : parsedToken.aud;
- const newResource = this.resource.endsWith("/")
- ? this.resource.slice(0, -1)
- : this.resource;
- const result = this._isAzureResourceManagerEndpoint(newResource, currentResource)
- ? false
- : currentResource !== newResource;
- return result;
- }
- /**
- * Gets the access token for the default or specified subscription.
- * @param options Optional parameters that can be provided to get the access token.
- */
- static async getAccessToken(options: AccessTokenOptions = {}): Promise<CliAccessToken> {
- try {
- let cmd = "account get-access-token";
- if (options.subscriptionIdOrName) {
- cmd += ` -s "${options.subscriptionIdOrName}"`;
- }
- if (options.resource) {
- cmd += ` --resource ${options.resource}`;
- }
- const result: any = await execAz(cmd);
- result.expiresOn = new Date(result.expiresOn);
- return result as CliAccessToken;
- } catch (err) {
- const message =
- `An error occurred while getting credentials from ` +
- `Azure CLI: ${err.stack}`;
- throw new Error(message);
- }
- }
- /**
- * Gets the subscription from Azure CLI.
- * @param subscriptionIdOrName - The name or id of the subscription for which the information is
- * required.
- */
- static async getSubscription(subscriptionIdOrName?: string): Promise<LinkedSubscription> {
- if (subscriptionIdOrName && (typeof subscriptionIdOrName !== "string" || !subscriptionIdOrName.length)) {
- throw new Error("'subscriptionIdOrName' must be a non-empty string.");
- }
- try {
- let cmd = "account show";
- if (subscriptionIdOrName) {
- cmd += ` -s "${subscriptionIdOrName}"`;
- }
- const result: LinkedSubscription = await execAz(cmd);
- return result;
- } catch (err) {
- const message =
- `An error occurred while getting information about the current subscription from ` +
- `Azure CLI: ${err.stack}`;
- throw new Error(message);
- }
- }
- /**
- * Sets the specified subscription as the default subscription for Azure CLI.
- * @param subscriptionIdOrName The name or id of the subsciption that needs to be set as the
- * default subscription.
- */
- static async setDefaultSubscription(subscriptionIdOrName: string): Promise<void> {
- try {
- await execAz(`account set -s ${subscriptionIdOrName}`);
- } catch (err) {
- const message =
- `An error occurred while setting the current subscription from ` +
- `Azure CLI: ${err.stack}`;
- throw new Error(message);
- }
- }
- /**
- * Returns a list of all the subscriptions from Azure CLI.
- * @param options Optional parameters that can be provided while listing all the subcriptions.
- */
- static async listAllSubscriptions(options: ListAllSubscriptionOptions = {}): Promise<LinkedSubscription[]> {
- let subscriptionList: any[] = [];
- try {
- let cmd = "account list";
- if (options.all) {
- cmd += " --all";
- }
- if (options.refresh) {
- cmd += "--refresh";
- }
- subscriptionList = await execAz(cmd);
- if (subscriptionList && subscriptionList.length) {
- for (const sub of subscriptionList) {
- if (sub.cloudName) {
- sub.environmentName = sub.cloudName;
- delete sub.cloudName;
- }
- }
- }
- return subscriptionList;
- } catch (err) {
- const message =
- `An error occurred while getting a list of all the subscription from ` +
- `Azure CLI: ${err.stack}`;
- throw new Error(message);
- }
- }
- /**
- * Provides credentials that can be used by the JS SDK to interact with Azure via azure cli.
- * **Pre-requisite**
- * - **install azure-cli** . For more information see
- * {@link https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest Install Azure CLI}
- * - **login via `az login`**
- * @param options - Optional parameters that can be provided while creating AzureCliCredentials.
- */
- static async create(options: AccessTokenOptions = {}): Promise<AzureCliCredentials> {
- const [subscriptinInfo, accessToken] = await Promise.all([
- AzureCliCredentials.getSubscription(options.subscriptionIdOrName),
- AzureCliCredentials.getAccessToken(options)
- ]);
- return new AzureCliCredentials(subscriptinInfo, accessToken, options.resource);
- }
- }
|