mirror of
https://github.com/tvytlx/ai-agent-deep-dive.git
synced 2026-04-04 16:14:50 +08:00
141 lines
8.4 KiB
JavaScript
141 lines
8.4 KiB
JavaScript
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
import { AuthenticationError, CredentialUnavailableError } from "../errors.js";
|
|
import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline";
|
|
import { ClientAssertionCredential } from "./clientAssertionCredential.js";
|
|
import { IdentityClient } from "../client/identityClient.js";
|
|
import { checkTenantId } from "../util/tenantIdUtils.js";
|
|
import { credentialLogger } from "../util/logging.js";
|
|
const credentialName = "AzurePipelinesCredential";
|
|
const logger = credentialLogger(credentialName);
|
|
const OIDC_API_VERSION = "7.1";
|
|
/**
|
|
* This credential is designed to be used in Azure Pipelines with service connections
|
|
* as a setup for workload identity federation.
|
|
*/
|
|
export class AzurePipelinesCredential {
|
|
/**
|
|
* AzurePipelinesCredential supports Federated Identity on Azure Pipelines through Service Connections.
|
|
* @param tenantId - tenantId associated with the service connection
|
|
* @param clientId - clientId associated with the service connection
|
|
* @param serviceConnectionId - Unique ID for the service connection, as found in the querystring's resourceId key
|
|
* @param systemAccessToken - The pipeline's <see href="https://learn.microsoft.com/azure/devops/pipelines/build/variables?view=azure-devops%26tabs=yaml#systemaccesstoken">System.AccessToken</see> value.
|
|
* @param options - The identity client options to use for authentication.
|
|
*/
|
|
constructor(tenantId, clientId, serviceConnectionId, systemAccessToken, options = {}) {
|
|
var _a, _b;
|
|
if (!clientId) {
|
|
throw new CredentialUnavailableError(`${credentialName}: is unavailable. clientId is a required parameter.`);
|
|
}
|
|
if (!tenantId) {
|
|
throw new CredentialUnavailableError(`${credentialName}: is unavailable. tenantId is a required parameter.`);
|
|
}
|
|
if (!serviceConnectionId) {
|
|
throw new CredentialUnavailableError(`${credentialName}: is unavailable. serviceConnectionId is a required parameter.`);
|
|
}
|
|
if (!systemAccessToken) {
|
|
throw new CredentialUnavailableError(`${credentialName}: is unavailable. systemAccessToken is a required parameter.`);
|
|
}
|
|
// Allow these headers to be logged for troubleshooting by AzurePipelines.
|
|
options.loggingOptions = Object.assign(Object.assign({}, options === null || options === void 0 ? void 0 : options.loggingOptions), { additionalAllowedHeaderNames: [
|
|
...((_b = (_a = options.loggingOptions) === null || _a === void 0 ? void 0 : _a.additionalAllowedHeaderNames) !== null && _b !== void 0 ? _b : []),
|
|
"x-vss-e2eid",
|
|
"x-msedge-ref",
|
|
] });
|
|
this.identityClient = new IdentityClient(options);
|
|
checkTenantId(logger, tenantId);
|
|
logger.info(`Invoking AzurePipelinesCredential with tenant ID: ${tenantId}, client ID: ${clientId}, and service connection ID: ${serviceConnectionId}`);
|
|
if (!process.env.SYSTEM_OIDCREQUESTURI) {
|
|
throw new CredentialUnavailableError(`${credentialName}: is unavailable. Ensure that you're running this task in an Azure Pipeline, so that following missing system variable(s) can be defined- "SYSTEM_OIDCREQUESTURI"`);
|
|
}
|
|
const oidcRequestUrl = `${process.env.SYSTEM_OIDCREQUESTURI}?api-version=${OIDC_API_VERSION}&serviceConnectionId=${serviceConnectionId}`;
|
|
logger.info(`Invoking ClientAssertionCredential with tenant ID: ${tenantId}, client ID: ${clientId} and service connection ID: ${serviceConnectionId}`);
|
|
this.clientAssertionCredential = new ClientAssertionCredential(tenantId, clientId, this.requestOidcToken.bind(this, oidcRequestUrl, systemAccessToken), options);
|
|
}
|
|
/**
|
|
* Authenticates with Microsoft Entra ID and returns an access token if successful.
|
|
* If authentication fails, a {@link CredentialUnavailableError} or {@link AuthenticationError} will be thrown with the details of the failure.
|
|
*
|
|
* @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.
|
|
*/
|
|
async getToken(scopes, options) {
|
|
if (!this.clientAssertionCredential) {
|
|
const errorMessage = `${credentialName}: is unavailable. To use Federation Identity in Azure Pipelines, the following parameters are required -
|
|
tenantId,
|
|
clientId,
|
|
serviceConnectionId,
|
|
systemAccessToken,
|
|
"SYSTEM_OIDCREQUESTURI".
|
|
See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/azurepipelinescredential/troubleshoot`;
|
|
logger.error(errorMessage);
|
|
throw new CredentialUnavailableError(errorMessage);
|
|
}
|
|
logger.info("Invoking getToken() of Client Assertion Credential");
|
|
return this.clientAssertionCredential.getToken(scopes, options);
|
|
}
|
|
/**
|
|
*
|
|
* @param oidcRequestUrl - oidc request url
|
|
* @param systemAccessToken - system access token
|
|
* @returns OIDC token from Azure Pipelines
|
|
*/
|
|
async requestOidcToken(oidcRequestUrl, systemAccessToken) {
|
|
logger.info("Requesting OIDC token from Azure Pipelines...");
|
|
logger.info(oidcRequestUrl);
|
|
const request = createPipelineRequest({
|
|
url: oidcRequestUrl,
|
|
method: "POST",
|
|
headers: createHttpHeaders({
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${systemAccessToken}`,
|
|
// Prevents the service from responding with a redirect HTTP status code (useful for automation).
|
|
"X-TFS-FedAuthRedirect": "Suppress",
|
|
}),
|
|
});
|
|
const response = await this.identityClient.sendRequest(request);
|
|
return handleOidcResponse(response);
|
|
}
|
|
}
|
|
export function handleOidcResponse(response) {
|
|
// OIDC token is present in `bodyAsText` field
|
|
const text = response.bodyAsText;
|
|
if (!text) {
|
|
logger.error(`${credentialName}: Authentication Failed. Received null token from OIDC request. Response status- ${response.status}. Complete response - ${JSON.stringify(response)}`);
|
|
throw new AuthenticationError(response.status, {
|
|
error: `${credentialName}: Authentication Failed. Received null token from OIDC request.`,
|
|
error_description: `${JSON.stringify(response)}. See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/azurepipelinescredential/troubleshoot`,
|
|
});
|
|
}
|
|
try {
|
|
const result = JSON.parse(text);
|
|
if (result === null || result === void 0 ? void 0 : result.oidcToken) {
|
|
return result.oidcToken;
|
|
}
|
|
else {
|
|
const errorMessage = `${credentialName}: Authentication Failed. oidcToken field not detected in the response.`;
|
|
let errorDescription = ``;
|
|
if (response.status !== 200) {
|
|
errorDescription = `Response body = ${text}. Response Headers ["x-vss-e2eid"] = ${response.headers.get("x-vss-e2eid")} and ["x-msedge-ref"] = ${response.headers.get("x-msedge-ref")}. See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/azurepipelinescredential/troubleshoot`;
|
|
}
|
|
logger.error(errorMessage);
|
|
logger.error(errorDescription);
|
|
throw new AuthenticationError(response.status, {
|
|
error: errorMessage,
|
|
error_description: errorDescription,
|
|
});
|
|
}
|
|
}
|
|
catch (e) {
|
|
const errorDetails = `${credentialName}: Authentication Failed. oidcToken field not detected in the response.`;
|
|
logger.error(`Response from service = ${text}, Response Headers ["x-vss-e2eid"] = ${response.headers.get("x-vss-e2eid")}
|
|
and ["x-msedge-ref"] = ${response.headers.get("x-msedge-ref")}, error message = ${e.message}`);
|
|
logger.error(errorDetails);
|
|
throw new AuthenticationError(response.status, {
|
|
error: errorDetails,
|
|
error_description: `Response = ${text}. Response headers ["x-vss-e2eid"] = ${response.headers.get("x-vss-e2eid")} and ["x-msedge-ref"] = ${response.headers.get("x-msedge-ref")}. See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/azurepipelinescredential/troubleshoot`,
|
|
});
|
|
}
|
|
}
|
|
//# sourceMappingURL=azurePipelinesCredential.js.map
|