/
visualStudioCodeCredential.ts
242 lines (209 loc) · 8.33 KB
/
visualStudioCodeCredential.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { TokenCredential, AccessToken, GetTokenOptions } from "@azure/core-auth";
import fs from "fs";
import os from "os";
import path from "path";
import { CredentialUnavailableError } from "../client/errors";
import { IdentityClient, TokenCredentialOptions } from "../client/identityClient";
import { AzureAuthorityHosts } from "../constants";
import { checkTenantId } from "../util/checkTenantId";
import { credentialLogger, formatError, formatSuccess } from "../util/logging";
import { processMultiTenantRequest } from "../util/validateMultiTenant";
import { VSCodeCredentialFinder } from "./visualStudioCodeCredentialExtension";
const CommonTenantId = "common";
const AzureAccountClientId = "aebc6443-996d-45c2-90f0-388ff96faa56"; // VSC: 'aebc6443-996d-45c2-90f0-388ff96faa56'
const logger = credentialLogger("VisualStudioCodeCredential");
let findCredentials: VSCodeCredentialFinder | undefined = undefined;
export const vsCodeCredentialControl = {
setVsCodeCredentialFinder(finder: VSCodeCredentialFinder): void {
findCredentials = finder;
}
};
// Map of unsupported Tenant IDs and the errors we will be throwing.
const unsupportedTenantIds: Record<string, string> = {
adfs: "The VisualStudioCodeCredential does not support authentication with ADFS tenants."
};
function checkUnsupportedTenant(tenantId: string): void {
// If the Tenant ID isn't supported, we throw.
const unsupportedTenantError = unsupportedTenantIds[tenantId];
if (unsupportedTenantError) {
throw new CredentialUnavailableError(unsupportedTenantError);
}
}
type VSCodeCloudNames = "AzureCloud" | "AzureChina" | "AzureGermanCloud" | "AzureUSGovernment";
const mapVSCodeAuthorityHosts: Record<VSCodeCloudNames, string> = {
AzureCloud: AzureAuthorityHosts.AzurePublicCloud,
AzureChina: AzureAuthorityHosts.AzureChina,
AzureGermanCloud: AzureAuthorityHosts.AzureGermany,
AzureUSGovernment: AzureAuthorityHosts.AzureGovernment
};
/**
* Attempts to load a specific property from the VSCode configurations of the current OS.
* If it fails at any point, returns undefined.
*/
export function getPropertyFromVSCode(property: string): string | undefined {
const settingsPath = ["User", "settings.json"];
// Eventually we can add more folders for more versions of VSCode.
const vsCodeFolder = "Code";
const homedir = os.homedir();
function loadProperty(...pathSegments: string[]): string | undefined {
const fullPath = path.join(...pathSegments, vsCodeFolder, ...settingsPath);
const settings = JSON.parse(fs.readFileSync(fullPath, { encoding: "utf8" }));
return settings[property];
}
try {
let appData: string;
switch (process.platform) {
case "win32":
appData = process.env.APPDATA!;
return appData ? loadProperty(appData) : undefined;
case "darwin":
return loadProperty(homedir, "Library", "Application Support");
case "linux":
return loadProperty(homedir, ".config");
default:
return;
}
} catch (e) {
logger.info(`Failed to load the Visual Studio Code configuration file. Error: ${e.message}`);
return;
}
}
/**
* Provides options to configure the Visual Studio Code credential.
*/
export interface VisualStudioCodeCredentialOptions extends TokenCredentialOptions {
/**
* Optionally pass in a Tenant ID to be used as part of the credential
*/
tenantId?: string;
}
/**
* Connect to Azure using the credential provided by the VSCode extension 'Azure Account'.
* Once the user has logged in via the extension, this credential can share the same refresh token
* that is cached by the extension.
*/
export class VisualStudioCodeCredential implements TokenCredential {
private identityClient: IdentityClient;
private tenantId: string;
private cloudName: VSCodeCloudNames;
private allowMultiTenantAuthentication?: boolean;
/**
* Creates an instance of VisualStudioCodeCredential to use for automatically authenticating via VSCode.
*
* @param options - Options for configuring the client which makes the authentication request.
*/
constructor(options?: VisualStudioCodeCredentialOptions) {
// We want to make sure we use the one assigned by the user on the VSCode settings.
// Or just `AzureCloud` by default.
this.cloudName = (getPropertyFromVSCode("azure.cloud") || "AzureCloud") as VSCodeCloudNames;
// Picking an authority host based on the cloud name.
const authorityHost = mapVSCodeAuthorityHosts[this.cloudName];
this.identityClient = new IdentityClient({
authorityHost,
...options
});
if (options && options.tenantId) {
checkTenantId(logger, options.tenantId);
this.tenantId = options.tenantId;
} else {
this.tenantId = CommonTenantId;
}
this.allowMultiTenantAuthentication = options?.allowMultiTenantAuthentication;
checkUnsupportedTenant(this.tenantId);
}
/**
* Runs preparations for any further getToken request.
*/
private async prepare(): Promise<void> {
// Attempts to load the tenant from the VSCode configuration file.
const settingsTenant = getPropertyFromVSCode("azure.tenant");
if (settingsTenant) {
this.tenantId = settingsTenant;
}
checkUnsupportedTenant(this.tenantId);
}
/**
* The promise of the single preparation that will be executed at the first getToken request for an instance of this class.
*/
private preparePromise: Promise<void> | undefined;
/**
* Runs preparations for any further getToken, but only once.
*/
private prepareOnce(): Promise<void> | undefined {
if (!this.preparePromise) {
this.preparePromise = this.prepare();
}
return this.preparePromise;
}
/**
* Returns the token found by searching VSCode's authentication cache or
* returns null if no token could be found.
*
* @param scopes - The list of scopes for which the token will have access.
* @param options - The options used to configure any requests this
* `TokenCredential` implementation might make.
*/
public async getToken(
scopes: string | string[],
options?: GetTokenOptions
): Promise<AccessToken> {
await this.prepareOnce();
const tenantId =
processMultiTenantRequest(this.tenantId, this.allowMultiTenantAuthentication, options) ||
this.tenantId;
if (findCredentials === undefined) {
throw new CredentialUnavailableError(
"No implementation of VisualStudioCodeCredential is available (do you need to install and use the `@azure/identity-vscode` extension package?)"
);
}
let scopeString = typeof scopes === "string" ? scopes : scopes.join(" ");
// Check to make sure the scope we get back is a valid scope
if (!scopeString.match(/^[0-9a-zA-Z-.:/]+$/)) {
const error = new Error("Invalid scope was specified by the user or calling client");
logger.getToken.info(formatError(scopes, error));
throw error;
}
if (scopeString.indexOf("offline_access") < 0) {
scopeString += " offline_access";
}
// findCredentials returns an array similar to:
// [
// {
// account: "",
// password: "",
// },
// /* ... */
// ]
const credentials = await findCredentials();
// If we can't find the credential based on the name, we'll pick the first one available.
const { password: refreshToken } =
credentials.find(({ account }) => account === this.cloudName) ?? credentials[0] ?? {};
if (refreshToken) {
const tokenResponse = await this.identityClient.refreshAccessToken(
tenantId,
AzureAccountClientId,
scopeString,
refreshToken,
undefined
);
if (tokenResponse) {
logger.getToken.info(formatSuccess(scopes));
return tokenResponse.accessToken;
} else {
const error = new CredentialUnavailableError(
"Could not retrieve the token associated with Visual Studio Code. Have you connected using the 'Azure Account' extension recently?"
);
logger.getToken.info(formatError(scopes, error));
throw error;
}
} else {
const error = new CredentialUnavailableError(
"Could not retrieve the token associated with Visual Studio Code. Did you connect using the 'Azure Account' extension?"
);
logger.getToken.info(formatError(scopes, error));
throw error;
}
}
}