Skip to content

Commit 57d9862

Browse files
auth: Add BearerChallengePolicy for MFA step-up challenges during subscription listing (#2231)
* auth: Add BearerChallengePolicy to handle MFA step-up challenges The auth package creates SubscriptionClient instances to list tenants and subscriptions, but these clients had no challenge policy on their HTTP pipeline. When users hit an MFA step-up challenge (401 with WWW-Authenticate header) during tenant/subscription listing, the request simply fails. Changes: - Create BearerChallengePolicy in auth/src/utils/BearerChallengePolicy.ts - Wire into SubscriptionClient pipeline in AzureSubscriptionProviderBase - Export BearerChallengePolicy and getDefaultScopeFromEndpoint - Add @azure/core-rest-pipeline as devDependency - Bump version to 6.0.0-alpha.6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR feedback: dep classification and retry tracking - Move @azure/core-rest-pipeline back to regular dependencies since BearerChallengePolicy is exported and its .d.ts references pipeline types - Replace HTTP header-based retry marker (x-azext-challenge-retry) with a WeakSet to track retried requests out-of-band, avoiding sending the marker over the wire Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 56bd4a2 commit 57d9862

File tree

5 files changed

+74
-11
lines changed

5 files changed

+74
-11
lines changed

auth/package-lock.json

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

auth/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@microsoft/vscode-azext-azureauth",
33
"author": "Microsoft Corporation",
4-
"version": "6.0.0-alpha.5",
4+
"version": "6.0.0-alpha.6",
55
"description": "Azure authentication helpers for Visual Studio Code",
66
"tags": [
77
"azure",
@@ -55,6 +55,7 @@
5555
},
5656
"dependencies": {
5757
"@azure/arm-resources-subscriptions": "^2.1.0",
58+
"@azure/core-rest-pipeline": "^1.22.2",
5859
"@azure/identity": "^4.13.0",
5960
"@azure/ms-rest-azure-env": "^2.0.0"
6061
},

auth/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './contracts/AzureTenant';
1212
// The `AzureDevOpsSubscriptionProvider` is intentionally not exported, it must be imported from `'@microsoft/vscode-azext-azureauth/azdo'`
1313
export * from './providers/AzureSubscriptionProviderBase';
1414
export * from './providers/VSCodeAzureSubscriptionProvider';
15+
export * from './utils/BearerChallengePolicy';
1516
export * from './utils/configuredAzureEnv';
1617
export * from './utils/dedupeSubscriptions';
1718
export * from './utils/getMetricsForTelemetry';

auth/src/providers/AzureSubscriptionProviderBase.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import type { SubscriptionClient } from '@azure/arm-resources-subscriptions'; // Keep this as `import type` to avoid actually loading the package before necessary
77
import type { GetTokenOptions, TokenCredential } from '@azure/core-auth'; // Keep this as `import type` to avoid actually loading the package (at all, this one is dev-only)
8+
import { BearerChallengePolicy } from '../utils/BearerChallengePolicy';
89
import { inspect } from 'util';
910
import * as vscode from 'vscode';
1011
import type { AzureAccount } from '../contracts/AzureAccount';
@@ -364,8 +365,23 @@ export abstract class AzureSubscriptionProviderBase implements AzureSubscription
364365

365366
armSubs ??= await import('@azure/arm-resources-subscriptions');
366367

368+
const endpoint = getConfiguredAzureEnv().resourceManagerEndpointUrl;
369+
const client = new armSubs.SubscriptionClient(credential, { endpoint });
370+
371+
client.pipeline.addPolicy(
372+
new BearerChallengePolicy(
373+
async (challenge) => {
374+
this.silenceRefreshEvents();
375+
const session = await getSessionFromVSCode(challenge, tenant.tenantId, { createIfNone: true, account: tenant.account });
376+
return session?.accessToken;
377+
},
378+
endpoint,
379+
),
380+
{ phase: 'Sign', afterPolicies: ['bearerTokenAuthenticationPolicy'] },
381+
);
382+
367383
return {
368-
client: new armSubs.SubscriptionClient(credential, { endpoint: getConfiguredAzureEnv().resourceManagerEndpointUrl }),
384+
client,
369385
credential: credential,
370386
authentication: {
371387
getSession: async () => {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { PipelinePolicy, PipelineRequest, PipelineResponse, SendRequest } from '@azure/core-rest-pipeline';
7+
import type * as vscode from 'vscode';
8+
9+
export function getDefaultScopeFromEndpoint(endpoint?: string): string {
10+
let base = endpoint ?? 'https://management.azure.com/';
11+
base = base.replace(/\/+$/, '');
12+
return `${base}/.default`;
13+
}
14+
15+
const challengeRetriedRequests = new WeakSet<PipelineRequest>();
16+
17+
export class BearerChallengePolicy implements PipelinePolicy {
18+
public readonly name = 'BearerChallengePolicy';
19+
20+
public constructor(
21+
private readonly getTokenForChallenge: (request: vscode.AuthenticationWwwAuthenticateRequest) => Promise<string | undefined>,
22+
private readonly endpoint?: string,
23+
) { }
24+
25+
public async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
26+
const initial = await next(request);
27+
28+
if (initial.status === 401 && !challengeRetriedRequests.has(request)) {
29+
const header = initial.headers.get('WWW-Authenticate') || initial.headers.get('www-authenticate');
30+
if (header) {
31+
const scopes = [getDefaultScopeFromEndpoint(this.endpoint)];
32+
challengeRetriedRequests.add(request);
33+
34+
const token = await this.getTokenForChallenge({ wwwAuthenticate: header, fallbackScopes: scopes });
35+
if (token) {
36+
request.headers.set('Authorization', `Bearer ${token}`);
37+
return await next(request);
38+
}
39+
}
40+
}
41+
42+
return initial;
43+
}
44+
}

0 commit comments

Comments
 (0)