Skip to content

Commit c5dcaee

Browse files
Jonathan TurnertzhanlzzhxiaofengJerryPei1997
authored
Cli credentials (#7637)
* change autorest.typescript version (#6519) (#6520) * update for cliCredential * update for cliCredential * Cleanup and get building * Clean up api exports a bit * Remove unused span creation * Use a protected method that's overridden rather than a set of interfaces * Use a protected method that's overridden rather than a set of interfaces * Improve the regex match Co-authored-by: Zhanle Tu (MSFT) <35680310+tzhanl@users.noreply.github.com> Co-authored-by: Ziheng Zhou(MSFT) <v-zihz@microsoft.com> Co-authored-by: Ruijie Pei (MSFT) <49467823+JerryPei1997@users.noreply.github.com>
1 parent 97c1adf commit c5dcaee

9 files changed

Lines changed: 229 additions & 2 deletions

File tree

sdk/identity/identity/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"types": "./types/identity.d.ts",
99
"browser": {
1010
"stream": "./node_modules/stream-browserify/index.js",
11+
"./dist-esm/src/credentials/azureCliCredential.js": "./dist-esm/src/credentials/azureCliCredential.browser.js",
1112
"./dist-esm/src/credentials/environmentCredential.js": "./dist-esm/src/credentials/environmentCredential.browser.js",
1213
"./dist-esm/src/credentials/managedIdentityCredential.js": "./dist-esm/src/credentials/managedIdentityCredential.browser.js",
1314
"./dist-esm/src/credentials/clientCertificateCredential.js": "./dist-esm/src/credentials/clientCertificateCredential.browser.js",

sdk/identity/identity/review/identity.api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export class AuthorizationCodeCredential implements TokenCredential {
3737
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken | null>;
3838
}
3939

40+
// @public
41+
export class AzureCliCredential implements TokenCredential {
42+
constructor();
43+
protected getAzureCliAccessToken(resource: string): Promise<unknown>;
44+
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken | null>;
45+
}
46+
4047
// @public
4148
export type BrowserLoginStyle = "redirect" | "popup";
4249

sdk/identity/identity/rollup.base.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const input = "dist-esm/src/index.js";
1212
const production = process.env.NODE_ENV === "production";
1313

1414
export function nodeConfig(test = false) {
15-
const externalNodeBuiltins = ["crypto", "events", "fs"];
15+
const externalNodeBuiltins = ["crypto", "events", "fs", "child-process"];
1616
const baseConfig = {
1717
input: input,
1818
external: depNames.concat(externalNodeBuiltins),
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/* eslint-disable @typescript-eslint/no-unused-vars */
5+
6+
import { AccessToken, TokenCredential, GetTokenOptions } from "@azure/core-http";
7+
import { TokenCredentialOptions } from "../client/identityClient";
8+
9+
const BrowserNotSupportedError = new Error("AzureCliCredential is not supported in the browser.");
10+
11+
export class AzureCliCredential implements TokenCredential {
12+
constructor(options?: TokenCredentialOptions) {
13+
throw BrowserNotSupportedError;
14+
}
15+
16+
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken | null> {
17+
throw BrowserNotSupportedError;
18+
}
19+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { TokenCredential, GetTokenOptions, AccessToken } from "@azure/core-http";
5+
import { createSpan } from "../util/tracing";
6+
import { AuthenticationErrorName } from "../client/errors";
7+
import { CanonicalCode } from "@opentelemetry/types";
8+
import { logger } from "../util/logging";
9+
10+
import * as child_process from "child_process";
11+
12+
/**
13+
* Provides the user access token and expire time
14+
* with Azure CLI command "az account get-access-token".
15+
*/
16+
export class AzureCliCredential implements TokenCredential {
17+
/**
18+
* Creates an instance of the AzureCliCredential class.
19+
*/
20+
constructor() {}
21+
22+
/**
23+
* Gets the access token from Azure CLI
24+
* @param resource The resource to use when getting the token
25+
*/
26+
protected async getAzureCliAccessToken(resource: string) {
27+
return new Promise((resolve, reject) => {
28+
try {
29+
child_process.exec(
30+
`az account get-access-token --output json --resource ${resource}`,
31+
(error, stdout, stderr) => {
32+
resolve({ stdout: stdout, stderr: stderr });
33+
}
34+
);
35+
} catch (err) {
36+
reject(err);
37+
}
38+
});
39+
}
40+
41+
/**
42+
* Authenticates with Azure Active Directory and returns an access token if
43+
* successful. If authentication cannot be performed at this time, this method may
44+
* return null. If an error occurs during authentication, an {@link AuthenticationError}
45+
* containing failure details will be thrown.
46+
*
47+
* @param scopes The list of scopes for which the token will have access.
48+
* @param options The options used to configure any requests this
49+
* TokenCredential implementation might make.
50+
*/
51+
public async getToken(
52+
scopes: string | string[],
53+
options?: GetTokenOptions
54+
): Promise<AccessToken | null> {
55+
return new Promise((resolve, reject) => {
56+
let scope: string;
57+
scope = typeof scopes === "string" ? scopes : scopes[0];
58+
logger.info(`use the scope ${scope}`);
59+
const resource = scope.replace(/\/.default$/, "");
60+
let responseData = "";
61+
62+
const { span } = createSpan("AzureCliCredential-getToken", options);
63+
this.getAzureCliAccessToken(resource)
64+
.then((obj: any) => {
65+
if (obj.stderr) {
66+
let isLoginError = obj.stderr.match("(.*)az login(.*)");
67+
let isNotInstallError =
68+
obj.stderr.match("az:(.*)not found") ||
69+
obj.stderr.startsWith("'az' is not recognized");
70+
if (isNotInstallError) {
71+
throw new Error("Azure CLI could not be found. Please visit https://aka.ms/azure-cli for installation instructions and then, once installed, authenticate to your Azure account using 'az login'.");
72+
} else if (isLoginError) {
73+
throw new Error("Please run 'az login' from a command prompt to authenticate before using this credential.");
74+
}
75+
throw new Error(obj.stderr);
76+
} else {
77+
responseData = obj.stdout;
78+
const response: { accessToken: string; expiresOn: string } = JSON.parse(responseData);
79+
resolve({
80+
token: response.accessToken,
81+
expiresOnTimestamp: new Date(response.expiresOn).getTime()
82+
});
83+
}
84+
})
85+
.catch((err) => {
86+
const code =
87+
err.name === AuthenticationErrorName
88+
? CanonicalCode.UNAUTHENTICATED
89+
: CanonicalCode.UNKNOWN;
90+
span.setStatus({
91+
code,
92+
message: err.message
93+
});
94+
reject(err);
95+
});
96+
});
97+
}
98+
}

sdk/identity/identity/src/credentials/defaultAzureCredential.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TokenCredentialOptions } from "../client/identityClient";
55
import { ChainedTokenCredential } from "./chainedTokenCredential";
66
import { EnvironmentCredential } from "./environmentCredential";
77
import { ManagedIdentityCredential } from "./managedIdentityCredential";
8+
import { AzureCliCredential } from "./azureCliCredential";
89

910
/**
1011
* Provides a default {@link ChainedTokenCredential} configuration for
@@ -26,7 +27,8 @@ export class DefaultAzureCredential extends ChainedTokenCredential {
2627
constructor(tokenCredentialOptions?: TokenCredentialOptions) {
2728
super(
2829
new EnvironmentCredential(tokenCredentialOptions),
29-
new ManagedIdentityCredential(tokenCredentialOptions)
30+
new ManagedIdentityCredential(tokenCredentialOptions),
31+
new AzureCliCredential()
3032
);
3133
}
3234
}

sdk/identity/identity/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { DefaultAzureCredential } from "./credentials/defaultAzureCredential";
77
export { ChainedTokenCredential } from "./credentials/chainedTokenCredential";
88
export { TokenCredentialOptions } from "./client/identityClient";
99
export { EnvironmentCredential } from "./credentials/environmentCredential";
10+
export { AzureCliCredential } from "./credentials/azureCliCredential";
1011
export { ClientSecretCredential } from "./credentials/clientSecretCredential";
1112
export { ClientCertificateCredential } from "./credentials/clientCertificateCredential";
1213
export { InteractiveBrowserCredential } from "./credentials/interactiveBrowserCredential";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
AzureCliCredential
3+
} from "../src/credentials/azureCliCredential";
4+
5+
interface MockCredentialClient {
6+
stdout: string;
7+
stderr: string;
8+
}
9+
10+
export class MockAzureCliCredentialClient extends AzureCliCredential {
11+
private stdout: string;
12+
private stderr: string;
13+
14+
constructor(options: MockCredentialClient) {
15+
super();
16+
this.stdout = options.stdout;
17+
this.stderr = options.stderr;
18+
}
19+
20+
/**
21+
* Replace the work of getting the access token with a mocked method
22+
* that will used mocked data instead of the output from the real `az`
23+
* command.
24+
* @param resource The resources to use when accessing token
25+
*/
26+
protected getAzureCliAccessToken(resource: string) {
27+
return new Promise((resolve) => {
28+
resolve({ stdout: this.stdout, stderr: this.stderr });
29+
});
30+
}
31+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import assert from "assert";
5+
import {
6+
MockAzureCliCredentialClient
7+
} from "../mockAzureCliCredentialClient";
8+
9+
describe("AzureCliCredential", function() {
10+
it("get access token without error", async function() {
11+
var mockCliCredentialClient = new MockAzureCliCredentialClient({
12+
stdout: '{"accessToken": "token","expiresOn": "01/01/1900 00:00:00 +00:00"}',
13+
stderr: ""
14+
});
15+
let actualToken = await mockCliCredentialClient.getToken("https://service/.default");
16+
assert.equal(actualToken!.token, "token");
17+
});
18+
19+
it("get access token when azure cli not installed", async () => {
20+
if (process.platform == "linux" || process.platform == "darwin") {
21+
var mockCliCredentialClient = new MockAzureCliCredentialClient({
22+
stdout: "",
23+
stderr: "az: command not found"
24+
});
25+
26+
try {
27+
await mockCliCredentialClient.getToken("https://service/.default");
28+
} catch (error) {
29+
assert.equal(error.message, "Azure CLI could not be found. Please visit https://aka.ms/azure-cli for installation instructions and then, once installed, authenticate to your Azure account using 'az login'.");
30+
}
31+
} else {
32+
var mockCliCredentialClient = new MockAzureCliCredentialClient({
33+
stdout: "",
34+
stderr: "'az' is not recognized"
35+
});
36+
37+
try {
38+
await mockCliCredentialClient.getToken("https://service/.default");
39+
} catch (error) {
40+
assert.equal(error.message, "Azure CLI could not be found. Please visit https://aka.ms/azure-cli for installation instructions and then, once installed, authenticate to your Azure account using 'az login'.");
41+
}
42+
}
43+
});
44+
45+
it("get access token when azure cli not login in", async () => {
46+
var mockCliCredentialClient = new MockAzureCliCredentialClient({
47+
stdout: "",
48+
stderr: "Please run 'az login' from a command prompt to authenticate before using this credential."
49+
});
50+
try {
51+
await mockCliCredentialClient.getToken("https://service/.default");
52+
} catch (error) {
53+
assert.equal(error.message, "Please run 'az login' from a command prompt to authenticate before using this credential.");
54+
}
55+
});
56+
57+
it("get access token when having other access token error", async () => {
58+
var mockCliCredentialClient = new MockAzureCliCredentialClient({
59+
stdout: "",
60+
stderr: "mock other access token error"
61+
});
62+
try {
63+
await mockCliCredentialClient.getToken("https://service/.default");
64+
} catch (error) {
65+
assert.equal(error.message, "mock other access token error");
66+
}
67+
});
68+
});

0 commit comments

Comments
 (0)