Skip to content

Commit d8407ad

Browse files
authored
[Identity] Support for tenant Id Challenges / tenant discovery in ClientCredentials (Azure#15837)
This PR adds `tenantId` to the `getTokenOptions`, and adds options on every Identity credential to allow multi-tenant authentication (which will be disabled by default). Fixes Azure#15797
1 parent 761a4e6 commit d8407ad

25 files changed

+345
-44
lines changed

sdk/core/core-auth/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 1.3.1 (2021-06-30)
44

5+
- Added `tenantId` optional property to the `GetTokenOptions` interface. If `tenantId` is set, credentials will be able to use multi-tenant authentication, in the cases when it's enabled.
56

67
## 1.3.0 (2021-03-30)
78

sdk/core/core-auth/review/core-auth.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface GetTokenOptions {
4747
requestOptions?: {
4848
timeout?: number;
4949
};
50+
tenantId?: string;
5051
tracingOptions?: {
5152
spanOptions?: SpanOptions;
5253
tracingContext?: Context;

sdk/core/core-auth/src/tokenCredential.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ export interface GetTokenOptions {
5252
*/
5353
tracingContext?: Context;
5454
};
55+
56+
/**
57+
* Allows specifying a tenantId. Useful to handle challenges that provide tenant Id hints.
58+
*/
59+
tenantId?: string;
5560
}
5661

5762
/**

sdk/identity/identity/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
- Added `regionalAuthority` property to `ClientSecretCredentialOptions` and `ClientCertificateCredentialOptions`.
1818
- If instead of a region, `AutoDiscoverRegion` is specified as the value for `regionalAuthority`, MSAL will be used to attempt to discover the region.
1919
- A region can also be specified through the `AZURE_REGIONAL_AUTHORITY_NAME` environment variable.
20+
- `AzureCliCredential` and `AzurePowerShellCredential` now allow specifying a `tenantId`.
21+
- All credentials except `ManagedIdentityCredential` support enabling multi tenant authentication via the `allowMultiTenantAuthentication` option.
2022

2123
### Breaking Changes
2224

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,24 @@ export enum AzureAuthorityHosts {
6666

6767
// @public
6868
export class AzureCliCredential implements TokenCredential {
69+
constructor(options?: AzureCliCredentialOptions);
6970
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
71+
}
72+
73+
// @public
74+
export interface AzureCliCredentialOptions extends TokenCredentialOptions {
75+
tenantId?: string;
7076
}
7177

7278
// @public
7379
export class AzurePowerShellCredential implements TokenCredential {
80+
constructor(options?: AzurePowerShellCredentialOptions);
7481
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken | null>;
82+
}
83+
84+
// @public
85+
export interface AzurePowerShellCredentialOptions extends TokenCredentialOptions {
86+
tenantId?: string;
7587
}
7688

7789
// @public
@@ -297,6 +309,7 @@ export { TokenCredential }
297309

298310
// @public
299311
export interface TokenCredentialOptions extends PipelineOptions {
312+
allowMultiTenantAuthentication?: boolean;
300313
authorityHost?: string;
301314
}
302315

@@ -316,7 +329,7 @@ export interface UsernamePasswordCredentialOptions extends TokenCredentialOption
316329
// @public
317330
export class VisualStudioCodeCredential implements TokenCredential {
318331
constructor(options?: VisualStudioCodeCredentialOptions);
319-
getToken(scopes: string | string[], _options?: GetTokenOptions): Promise<AccessToken>;
332+
getToken(scopes: string | string[], options?: GetTokenOptions): Promise<AccessToken>;
320333
}
321334

322335
// @public

sdk/identity/identity/src/client/identityClient.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,9 @@ export interface TokenCredentialOptions extends PipelineOptions {
313313
* The default is "https://login.microsoftonline.com".
314314
*/
315315
authorityHost?: string;
316+
317+
/**
318+
* If set to true, allows authentication flows to change the tenantId of the request if a different tenantId is received from a challenge or through a direct getToken call.
319+
*/
320+
allowMultiTenantAuthentication?: boolean;
316321
}

sdk/identity/identity/src/credentials/authorizationCodeCredential.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SpanStatusCode } from "@azure/core-tracing";
1212
import { credentialLogger, formatSuccess, formatError } from "../util/logging";
1313
import { getIdentityTokenEndpointSuffix } from "../util/identityTokenEndpoint";
1414
import { checkTenantId } from "../util/checkTenantId";
15+
import { processMultiTenantRequest } from "../util/validateMultiTenant";
1516

1617
const logger = credentialLogger("AuthorizationCodeCredential");
1718

@@ -30,6 +31,7 @@ export class AuthorizationCodeCredential implements TokenCredential {
3031
private authorizationCode: string;
3132
private redirectUri: string;
3233
private lastTokenResponse: TokenResponse | null = null;
34+
private allowMultiTenantAuthentication?: boolean;
3335

3436
/**
3537
* Creates an instance of CodeFlowCredential with the details needed
@@ -120,6 +122,7 @@ export class AuthorizationCodeCredential implements TokenCredential {
120122
options = redirectUriOrOptions as TokenCredentialOptions;
121123
}
122124

125+
this.allowMultiTenantAuthentication = options?.allowMultiTenantAuthentication;
123126
this.identityClient = new IdentityClient(options);
124127
}
125128

@@ -135,6 +138,10 @@ export class AuthorizationCodeCredential implements TokenCredential {
135138
scopes: string | string[],
136139
options?: GetTokenOptions
137140
): Promise<AccessToken> {
141+
const tenantId =
142+
processMultiTenantRequest(this.tenantId, this.allowMultiTenantAuthentication, options) ||
143+
this.tenantId;
144+
138145
const { span, updatedOptions } = createSpan("AuthorizationCodeCredential-getToken", options);
139146
try {
140147
let tokenResponse: TokenResponse | null = null;
@@ -146,7 +153,7 @@ export class AuthorizationCodeCredential implements TokenCredential {
146153
// Try to use the refresh token first
147154
if (this.lastTokenResponse && this.lastTokenResponse.refreshToken) {
148155
tokenResponse = await this.identityClient.refreshAccessToken(
149-
this.tenantId,
156+
tenantId,
150157
this.clientId,
151158
scopeString,
152159
this.lastTokenResponse.refreshToken,
@@ -157,9 +164,9 @@ export class AuthorizationCodeCredential implements TokenCredential {
157164
}
158165

159166
if (tokenResponse === null) {
160-
const urlSuffix = getIdentityTokenEndpointSuffix(this.tenantId);
167+
const urlSuffix = getIdentityTokenEndpointSuffix(tenantId);
161168
const webResource = this.identityClient.createWebResource({
162-
url: `${this.identityClient.authorityHost}/${this.tenantId}/${urlSuffix}`,
169+
url: `${this.identityClient.authorityHost}/${tenantId}/${urlSuffix}`,
163170
method: "POST",
164171
disableJsonStringifyOnBody: true,
165172
deserializationMapper: undefined,

sdk/identity/identity/src/credentials/azureCliCredential.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { SpanStatusCode } from "@azure/core-tracing";
99
import { credentialLogger, formatSuccess, formatError } from "../util/logging";
1010
import * as child_process from "child_process";
1111
import { ensureValidScope, getScopeResource } from "../util/scopeUtils";
12+
import { AzureCliCredentialOptions } from "./azureCliCredentialOptions";
13+
import { processMultiTenantRequest } from "../util/validateMultiTenant";
14+
import { checkTenantId } from "../util/checkTenantId";
1215

1316
/**
1417
* Mockable reference to the CLI credential cliCredentialFunctions
@@ -35,13 +38,26 @@ export const cliCredentialInternals = {
3538
* @internal
3639
*/
3740
async getAzureCliAccessToken(
38-
resource: string
41+
resource: string,
42+
tenantId?: string
3943
): Promise<{ stdout: string; stderr: string; error: Error | null }> {
44+
let tenantSection: string[] = [];
45+
if (tenantId) {
46+
tenantSection = ["--tenant", tenantId];
47+
}
4048
return new Promise((resolve, reject) => {
4149
try {
4250
child_process.execFile(
4351
"az",
44-
["account", "get-access-token", "--output", "json", "--resource", resource],
52+
[
53+
"account",
54+
"get-access-token",
55+
"--output",
56+
"json",
57+
"--resource",
58+
...tenantSection,
59+
resource
60+
],
4561
{ cwd: cliCredentialInternals.getSafeWorkingDir() },
4662
(error, stdout, stderr) => {
4763
resolve({ stdout: stdout, stderr: stderr, error });
@@ -65,6 +81,19 @@ const logger = credentialLogger("AzureCliCredential");
6581
* in via the 'az' tool using the command "az login" from the commandline.
6682
*/
6783
export class AzureCliCredential implements TokenCredential {
84+
private tenantId?: string;
85+
private allowMultiTenantAuthentication?: boolean;
86+
87+
/**
88+
* Creates an instance of the {@link AzureCliCredential}.
89+
*
90+
* @param options - Options, to optionally allow multi-tenant requests.
91+
*/
92+
constructor(options?: AzureCliCredentialOptions) {
93+
this.tenantId = options?.tenantId;
94+
this.allowMultiTenantAuthentication = options?.allowMultiTenantAuthentication;
95+
}
96+
6897
/**
6998
* Authenticates with Azure Active Directory and returns an access token if successful.
7099
* If authentication fails, a {@link CredentialUnavailableError} will be thrown with the details of the failure.
@@ -77,9 +106,17 @@ export class AzureCliCredential implements TokenCredential {
77106
scopes: string | string[],
78107
options?: GetTokenOptions
79108
): Promise<AccessToken> {
109+
const tenantId = processMultiTenantRequest(
110+
this.tenantId,
111+
this.allowMultiTenantAuthentication,
112+
options
113+
);
114+
if (tenantId) {
115+
checkTenantId(logger, tenantId);
116+
}
117+
80118
const scope = typeof scopes === "string" ? scopes : scopes[0];
81119
logger.getToken.info(`Using the scope ${scope}`);
82-
83120
ensureValidScope(scope, logger);
84121
const resource = getScopeResource(scope);
85122

@@ -88,7 +125,7 @@ export class AzureCliCredential implements TokenCredential {
88125
const { span } = createSpan("AzureCliCredential-getToken", options);
89126

90127
try {
91-
const obj = await cliCredentialInternals.getAzureCliAccessToken(resource);
128+
const obj = await cliCredentialInternals.getAzureCliAccessToken(resource, tenantId);
92129
if (obj.stderr) {
93130
const isLoginError = obj.stderr.match("(.*)az login(.*)");
94131
const isNotInstallError =
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { TokenCredentialOptions } from "../client/identityClient";
5+
6+
/**
7+
* Options for the {@link AzureCliCredential}
8+
*/
9+
export interface AzureCliCredentialOptions extends TokenCredentialOptions {
10+
/**
11+
* Allows specifying a tenant ID
12+
*/
13+
tenantId?: string;
14+
}

sdk/identity/identity/src/credentials/azurePowerShellCredential.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { credentialLogger, formatSuccess, formatError } from "../util/logging";
88
import { trace } from "../util/tracing";
99
import { ensureValidScope, getScopeResource } from "../util/scopeUtils";
1010
import { processUtils } from "../util/processUtils";
11+
import { AzurePowerShellCredentialOptions } from "./azurePowerShellCredentialOptions";
12+
import { processMultiTenantRequest } from "../util/validateMultiTenant";
13+
import { checkTenantId } from "../util/checkTenantId";
1114

1215
const logger = credentialLogger("AzurePowerShellCredential");
1316

@@ -92,12 +95,26 @@ if (isWindows) {
9295
* `Connect-AzAccount` from the command line.
9396
*/
9497
export class AzurePowerShellCredential implements TokenCredential {
98+
private tenantId?: string;
99+
private allowMultiTenantAuthentication?: boolean;
100+
101+
/**
102+
* Creates an instance of the {@link AzurePowershellCredential}.
103+
*
104+
* @param options - Options, to optionally allow multi-tenant requests.
105+
*/
106+
constructor(options?: AzurePowerShellCredentialOptions) {
107+
this.tenantId = options?.tenantId;
108+
this.allowMultiTenantAuthentication = options?.allowMultiTenantAuthentication;
109+
}
110+
95111
/**
96112
* Gets the access token from Azure PowerShell
97113
* @param resource - The resource to use when getting the token
98114
*/
99115
private async getAzurePowerShellAccessToken(
100-
resource: string
116+
resource: string,
117+
tenantId?: string
101118
): Promise<{ Token: string; ExpiresOn: string }> {
102119
// Clone the stack to avoid mutating it while iterating
103120
for (const powerShellCommand of [...commandStack]) {
@@ -109,6 +126,11 @@ export class AzurePowerShellCredential implements TokenCredential {
109126
continue;
110127
}
111128

129+
let tenantSection = "";
130+
if (tenantId) {
131+
tenantSection = `-TenantId "${tenantId}"`;
132+
}
133+
112134
const results = await runCommands([
113135
[
114136
powerShellCommand,
@@ -118,7 +140,7 @@ export class AzurePowerShellCredential implements TokenCredential {
118140
[
119141
powerShellCommand,
120142
"-Command",
121-
`Get-AzAccessToken -ResourceUrl "${resource}" | ConvertTo-Json`
143+
`Get-AzAccessToken ${tenantSection} -ResourceUrl "${resource}" | ConvertTo-Json`
122144
]
123145
]);
124146

@@ -145,15 +167,22 @@ export class AzurePowerShellCredential implements TokenCredential {
145167
options: GetTokenOptions = {}
146168
): Promise<AccessToken | null> {
147169
return trace(`${this.constructor.name}.getToken`, options, async () => {
148-
const scope = typeof scopes === "string" ? scopes : scopes[0];
149-
150-
logger.getToken.info(`Using the scope ${scope}`);
170+
const tenantId = processMultiTenantRequest(
171+
this.tenantId,
172+
this.allowMultiTenantAuthentication,
173+
options
174+
);
175+
if (tenantId) {
176+
checkTenantId(logger, tenantId);
177+
}
151178

179+
const scope = typeof scopes === "string" ? scopes : scopes[0];
152180
ensureValidScope(scope, logger);
181+
logger.getToken.info(`Using the scope ${scope}`);
153182
const resource = getScopeResource(scope);
154183

155184
try {
156-
const response = await this.getAzurePowerShellAccessToken(resource);
185+
const response = await this.getAzurePowerShellAccessToken(resource, tenantId);
157186
logger.getToken.info(formatSuccess(scopes));
158187
return {
159188
token: response.Token,

0 commit comments

Comments
 (0)