@azure/communication-common
Version:
Common package for Azure Communication services.
128 lines • 5.43 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { getClient } from "@azure-rest/core-client";
import { createDefaultHttpClient, createHttpHeaders, createPipelineRequest, } from "@azure/core-rest-pipeline";
const TeamsExtensionScopePrefixes = [
"https://auth.msft.communication.azure.com/",
"https://auth.msft.communication.azure.us/",
];
const CommunicationClientsScopePrefix = "https://communication.azure.com/clients/";
const ScopeValidationErrorMessage = `Scopes validation failed. Ensure all scopes start with one of ${[
...TeamsExtensionScopePrefixes,
CommunicationClientsScopePrefix,
].join(", ")}.`;
const TeamsExtensionEndpoint = "/access/teamsExtension/:exchangeAccessToken";
const TeamsExtensionApiVersion = "2025-06-30";
const CommunicationClientsEndpoint = "/access/entra/:exchangeAccessToken";
const CommunicationClientsApiVersion = "2025-03-02-preview";
/**
* Represents a credential that exchanges an Entra token for an Azure Communication Services (ACS) token, enabling access to ACS resources.
*/
export class EntraTokenCredential {
options;
isPending;
result = {
entraToken: undefined,
acsToken: { token: "", expiresOnTimestamp: 0 },
};
client;
httpClient;
constructor(options) {
this.options = options;
this.client = getClient(options.resourceEndpoint);
this.httpClient = createDefaultHttpClient();
this.options = options;
this.options.scopes = this.options.scopes || [
"https://communication.azure.com/clients/.default",
];
// immediately fetch the token to pre-warm
this.isPending = this.getToken();
}
async getToken(options) {
if (options?.abortSignal?.aborted) {
return { token: "", expiresOnTimestamp: 0 };
}
// we're awaiting the token fetch, so we don't want to start another one
// however, we're ignoring the new abortSignal, unfortunately
if (!this.isPending) {
this.isPending = this.getTokenInternal(options);
}
try {
await this.isPending;
}
finally {
this.isPending = null;
}
return this.result.acsToken;
}
async getTokenInternal(options) {
const getTokenOptions = options?.abortSignal ? { abortSignal: options.abortSignal } : undefined;
const token = await this.options.tokenCredential.getToken(this.options.scopes
? this.options.scopes
: ["https://communication.azure.com/clients/.default"], getTokenOptions);
const currentDateTime = new Date();
const tokenExpiresOn = new Date(this.result.acsToken.expiresOnTimestamp);
if (token === null) {
this.result = {
entraToken: undefined,
acsToken: { token: "", expiresOnTimestamp: 0 },
};
}
else if (this.result.acsToken.token === "" ||
token.token !== this.result.entraToken ||
tokenExpiresOn < currentDateTime) {
const acsToken = await this.exchangeEntraToken(this.options.resourceEndpoint, token.token, getTokenOptions);
this.result = {
entraToken: token.token,
acsToken,
};
}
return this.result.acsToken;
}
dispose() {
this.result = {
entraToken: undefined,
acsToken: { token: "", expiresOnTimestamp: 0 },
};
}
async exchangeEntraToken(resourceEndpoint, entraToken, options) {
const request = createPipelineRequest({
url: this.createRequestUri(resourceEndpoint),
method: "POST",
headers: createHttpHeaders({
Authorization: `Bearer ${entraToken}`,
"Content-Type": "application/json",
Accept: "application/json",
}),
abortSignal: options?.abortSignal,
body: JSON.stringify({}),
});
const response = await this.client.pipeline.sendRequest(this.httpClient, request);
if (response.status !== 200 || !response.bodyAsText) {
throw new Error(`Service request failed. Status: ${response.status}, Body: ${response.bodyAsText}`);
}
const json = JSON.parse(response.bodyAsText);
return {
token: json.accessToken.token,
expiresOnTimestamp: Date.parse(json.accessToken.expiresOn),
};
}
createRequestUri(resourceEndpoint) {
const [endpoint, apiVersion] = this.determineEndpointAndApiVersion();
const requestUri = `${resourceEndpoint}${endpoint}?api-version=${apiVersion}`;
return requestUri;
}
determineEndpointAndApiVersion() {
if (!this.options.scopes || this.options.scopes.length === 0) {
throw new Error(ScopeValidationErrorMessage);
}
if (this.options.scopes.every((scope) => TeamsExtensionScopePrefixes.some((prefix) => scope.startsWith(prefix)))) {
return [TeamsExtensionEndpoint, TeamsExtensionApiVersion];
}
if (this.options.scopes.every((scope) => scope.startsWith(CommunicationClientsScopePrefix))) {
return [CommunicationClientsEndpoint, CommunicationClientsApiVersion];
}
throw new Error(ScopeValidationErrorMessage);
}
}
//# sourceMappingURL=entraTokenCredential.js.map