Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions azure/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@azure/core-client": "^1.10.1",
"@azure/core-rest-pipeline": "^1.22.2",
"@azure/logger": "^1.3.0",
"@microsoft/vscode-azext-azureauth": "^6.0.0-alpha.6",
Comment thread
alexweininger marked this conversation as resolved.
"@microsoft/vscode-azext-utils": "^4.0.4",
"@microsoft/vscode-azureresources-api": "^3.1.0",
"semver": "^7.7.4"
Expand Down
84 changes: 22 additions & 62 deletions azure/src/createAzureClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { ServiceClient } from '@azure/core-client';
import { createHttpHeaders, createPipelineRequest, defaultRetryPolicy, Pipeline, PipelineOptions, PipelinePolicy, PipelineRequest, PipelineResponse, RestError, RetryPolicyOptions, SendRequest, userAgentPolicy } from '@azure/core-rest-pipeline';
import { BearerChallengePolicy } from '@microsoft/vscode-azext-azureauth';
import { appendExtensionUserAgent, AzExtServiceClientCredentialsT2, AzExtTreeItem, IActionContext, ISubscriptionActionContext, ISubscriptionContext, parseError } from '@microsoft/vscode-azext-utils';
import { randomUUID } from 'crypto';
import { Agent as HttpsAgent } from 'https';
Expand Down Expand Up @@ -44,6 +45,25 @@ function getChallengeHandlerFromCredential(createCredentialsForScopes: (request:
return getTokenForChallenge;
}

function createBearerChallengePolicy(context: ISubscriptionActionContext): BearerChallengePolicy {
const handleChallenge = getChallengeHandlerFromCredential(context.createCredentialsForScopes);
return new BearerChallengePolicy(
async (challenge) => {
context.telemetry.properties.challenge = 'true';
try {
const token = await handleChallenge(challenge);
context.telemetry.properties.challengeSuccess = token ? 'true' : 'false';
return token;
} catch (err) {
context.telemetry.properties.challengeSuccess = 'false';
context.telemetry.properties.challengeError = parseError(err).message;
throw err;
}
},
context.environment.resourceManagerEndpointUrl,
);
}

export function createAzureClient<T extends ServiceClient>(clientContext: InternalAzExtClientContext, clientType: types.AzExtClientType<T>): T {
const context = parseClientContext(clientContext);
const client = new clientType(context.credentials, context.subscriptionId, {
Expand All @@ -57,11 +77,7 @@ export function createAzureClient<T extends ServiceClient>(clientContext: Intern
context.environment.resourceManagerEndpointUrl,
undefined,
undefined,
new AzExtBearerChallengePolicy(
context,
getChallengeHandlerFromCredential(context.createCredentialsForScopes),
context.environment.resourceManagerEndpointUrl
)
createBearerChallengePolicy(context)
);
return client;
}
Expand All @@ -79,11 +95,7 @@ export function createAzureSubscriptionClient<T extends ServiceClient>(clientCon
context.environment.resourceManagerEndpointUrl,
undefined,
undefined,
new AzExtBearerChallengePolicy(
context,
getChallengeHandlerFromCredential(context.createCredentialsForScopes),
context.environment.resourceManagerEndpointUrl
)
createBearerChallengePolicy(context)
);
return client;
}
Expand Down Expand Up @@ -317,55 +329,3 @@ class AllowInsecureConnectionPolicy implements PipelinePolicy {
return await next(request);
}
}

/**
* Resolve a default scope from the Resource Manager endpoint or any provided endpoint.
* Example: https://management.azure.com/ -> https://management.azure.com/.default
*/
function getDefaultScopeFromEndpoint(endpoint?: string): string {
let base = endpoint ?? 'https://management.azure.com/';
base = base.replace(/\/+$/, '');
return `${base}/.default`;
}

/**
* A custom bearer policy that pre-authorizes and then retries once on a 401 with a WWW-Authenticate challenge.
*/
class AzExtBearerChallengePolicy implements PipelinePolicy {
public readonly name = 'AzExtBearerChallengePolicy';
private readonly challengeRetryHeader = 'x-azext-challenge-retry';

public constructor(
private readonly context: IActionContext,
private readonly getTokenForChallenge: (request: vscode.AuthenticationWwwAuthenticateRequest) => Promise<string | undefined>,
private readonly endpoint?: string
) { }

public async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
const initial = await next(request);

// Only attempt a single retry on auth challenges
if ((initial.status === 401) && !request.headers.get(this.challengeRetryHeader)) {
const header = initial.headers.get('WWW-Authenticate') || initial.headers.get('www-authenticate');
if (header) {
this.context.telemetry.properties.challenge = 'true';
const scopes = [getDefaultScopeFromEndpoint(this.endpoint)];
// Mark the request as having attempted a challenge so that if the pipeline
// (or other policies like a retry policy) replays the request when token
// fetching fails, we don't attempt the challenge again.
request.headers.set(this.challengeRetryHeader, '1');

const token = await this.getTokenForChallenge({ wwwAuthenticate: header, fallbackScopes: scopes });
if (token) {
this.context.telemetry.properties.challengeSuccess = 'true';
request.headers.set('Authorization', `Bearer ${token}`);
return await next(request);
} else {
this.context.telemetry.properties.challengeSuccess = 'false';
}
}
}

return initial;
}
}
Loading