@dvsa/appdev-api-common
Version:
Utils library for common API functionality
111 lines (110 loc) • 4.44 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AwsOIDCAzureTokenClient = void 0;
const client_sts_1 = require("@aws-sdk/client-sts");
const jose_1 = require("jose");
class AwsOIDCAzureTokenClient {
tenantId;
clientId;
tokenDurationSeconds;
options;
static accessToken;
static stsClient = new client_sts_1.STSClient();
/**
* Create a new instance of the AwsToAzureFederatedCredentials class
* @param tenantId - The Azure AD tenant ID
* @param clientId - The Azure AD application (client) ID
* @param tokenDurationSeconds - Duration for the AWS token (default: 300)
* @param options - Credentials options
*/
constructor(tenantId, clientId, tokenDurationSeconds = 300, options = {
debugMode: false,
forceFreshAuth: false,
expirySkewSeconds: 30,
}) {
this.tenantId = tenantId;
this.clientId = clientId;
this.tokenDurationSeconds = tokenDurationSeconds;
this.options = options;
}
/**
* Returns an Azure AD access token for the configured application.
*
* Uses an in-memory cached token when it’s still valid; otherwise it obtains a fresh token
* by exchanging an AWS OIDC web identity JWT against the Azure v2 token endpoint.
*
* @returns {Promise<string>} - The Azure access token
*/
async getAccessToken() {
if (this.options?.forceFreshAuth ||
!AwsOIDCAzureTokenClient.accessToken ||
AwsOIDCAzureTokenClient.isAccessTokenExpired(this.options.expirySkewSeconds ?? 30)) {
const { access_token } = await this.fetchFederatedCredentials();
if (this.options?.debugMode) {
console.log("[DEBUG] New Azure access token fetched:", access_token);
}
AwsOIDCAzureTokenClient.accessToken = access_token;
}
else if (this.options?.debugMode) {
console.log("[DEBUG] Using existing Azure access token:", AwsOIDCAzureTokenClient.accessToken);
}
return AwsOIDCAzureTokenClient.accessToken;
}
/**
* Fetch the AWS JWT and exchange it for an Azure token
* @returns {Promise<AzureTokenResponse>} - The Azure token response
* @internal
*/
async fetchFederatedCredentials() {
const stsResponse = await AwsOIDCAzureTokenClient.stsClient.send(new client_sts_1.GetWebIdentityTokenCommand({
Audience: [this.clientId],
SigningAlgorithm: "RS256",
DurationSeconds: this.tokenDurationSeconds,
}));
const awsJwt = stsResponse?.WebIdentityToken;
if (!awsJwt) {
throw new Error("STS did not return a WebIdentityToken");
}
if (this.options?.debugMode) {
console.log("[DEBUG] AWS JWT obtained", awsJwt);
}
const searchParams = new URLSearchParams({
grant_type: "client_credentials",
client_id: this.clientId,
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: awsJwt,
scope: `api://${this.clientId}/.default`,
});
const response = await fetch(`https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: searchParams.toString(),
});
if (!response.ok) {
throw new Error(`Azure token endpoint error: HTTP ${response.status} - ${await response.text()}`);
}
return await response.json();
}
/**
* Check if the access token is expired
* @returns {boolean} - Whether the access token is expired
* @internal
*/
static isAccessTokenExpired(skewSeconds) {
let decodedAccessToken;
try {
decodedAccessToken = (0, jose_1.decodeJwt)(AwsOIDCAzureTokenClient.accessToken);
}
catch (err) {
console.error("Error decoding access token:", err);
return true;
}
const currentTime = Math.floor(Date.now() / 1000);
const exp = decodedAccessToken?.exp;
if (!exp)
return true;
// treat as expired if we're within the skew window
return currentTime >= exp - skewSeconds;
}
}
exports.AwsOIDCAzureTokenClient = AwsOIDCAzureTokenClient;