feat: refactored common OIDC code and moved to shared sources directory.
This commit is contained in:
@@ -1,90 +1,14 @@
|
||||
import * as tl from 'azure-pipelines-task-lib/task';
|
||||
|
||||
type OidcResponse = {
|
||||
oidcToken?: string;
|
||||
};
|
||||
|
||||
type TokenResponse = {
|
||||
access_token?: string;
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
};
|
||||
|
||||
const CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer';
|
||||
import {
|
||||
buildOidcUrl,
|
||||
exchangeOidcForScopedToken,
|
||||
getServiceConnectionMetadata,
|
||||
requireInput,
|
||||
requestOidcToken,
|
||||
requireVariable
|
||||
} from '../../_shared/src/oidc';
|
||||
const STORAGE_SCOPE = 'https://storage.azure.com/.default';
|
||||
|
||||
function requireInput(name: string): string {
|
||||
const value = tl.getInput(name, true);
|
||||
if (!value) {
|
||||
throw new Error(`Task input ${name} is required.`);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function requireVariable(name: string): string {
|
||||
const value = tl.getVariable(name);
|
||||
if (!value) {
|
||||
throw new Error(`Missing required variable: ${name}.`);
|
||||
}
|
||||
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function getServiceConnectionMetadata(endpointId: string): { tenantId: string; clientId: string } {
|
||||
const tenantId =
|
||||
tl.getEndpointAuthorizationParameter(endpointId, 'tenantid', true) ||
|
||||
tl.getEndpointDataParameter(endpointId, 'tenantid', true);
|
||||
|
||||
const clientId =
|
||||
tl.getEndpointAuthorizationParameter(endpointId, 'serviceprincipalid', true) ||
|
||||
tl.getEndpointAuthorizationParameter(endpointId, 'clientid', true) ||
|
||||
tl.getEndpointDataParameter(endpointId, 'serviceprincipalid', true);
|
||||
|
||||
if (!tenantId) {
|
||||
throw new Error('Could not resolve tenant ID from the selected AzureRM service connection.');
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
throw new Error('Could not resolve client ID from the selected AzureRM service connection.');
|
||||
}
|
||||
|
||||
return { tenantId, clientId };
|
||||
}
|
||||
|
||||
function buildOidcUrl(baseUrl: string, serviceConnectionId: string): string {
|
||||
const url = new URL(baseUrl);
|
||||
url.searchParams.set('api-version', '7.1');
|
||||
url.searchParams.set('serviceConnectionId', serviceConnectionId);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function requestOidcToken(requestUrl: string, accessToken: string): Promise<string> {
|
||||
const response = await fetch(requestUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': '0'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const responseBody = await response.text();
|
||||
throw new Error(
|
||||
`OIDC request failed with status ${response.status} ${response.statusText}. Response: ${responseBody}`
|
||||
);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as OidcResponse;
|
||||
const token = data.oidcToken?.trim();
|
||||
if (!token) {
|
||||
throw new Error('OIDC response did not include a non-empty oidcToken field.');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
function buildBlobUrl(accountName: string, containerName: string, blobName: string): string {
|
||||
const trimmedBlobName = blobName.replace(/^\/+/, '');
|
||||
const encodedContainer = encodeURIComponent(containerName);
|
||||
@@ -96,48 +20,6 @@ function buildBlobUrl(accountName: string, containerName: string, blobName: stri
|
||||
return `https://${accountName}.blob.core.windows.net/${encodedContainer}/${encodedBlobName}`;
|
||||
}
|
||||
|
||||
async function exchangeOidcForStorageToken(tenantId: string, clientId: string, oidcToken: string): Promise<string> {
|
||||
const tokenUrl = `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0/token`;
|
||||
const body = new URLSearchParams({
|
||||
client_id: clientId,
|
||||
scope: STORAGE_SCOPE,
|
||||
grant_type: 'client_credentials',
|
||||
client_assertion_type: CLIENT_ASSERTION_TYPE,
|
||||
client_assertion: oidcToken
|
||||
}).toString();
|
||||
|
||||
const response = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
const rawBody = await response.text();
|
||||
let parsed: TokenResponse = {};
|
||||
|
||||
if (rawBody.trim()) {
|
||||
try {
|
||||
parsed = JSON.parse(rawBody) as TokenResponse;
|
||||
} catch {
|
||||
parsed = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const details = parsed.error_description || parsed.error || rawBody || 'Unknown token exchange error.';
|
||||
throw new Error(`Token request failed (${response.status} ${response.statusText}): ${details}`);
|
||||
}
|
||||
|
||||
const token = parsed.access_token?.trim();
|
||||
if (!token) {
|
||||
throw new Error('Token exchange succeeded but access_token is missing.');
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
async function copyBlob(
|
||||
sourceUrl: string,
|
||||
destinationUrl: string,
|
||||
@@ -171,28 +53,32 @@ async function copyBlob(
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const endpointId = requireInput('serviceConnectionARM');
|
||||
const srcStorageAccountName = requireInput('srcStorageAccountName');
|
||||
const dstStorageAccountName = requireInput('dstStorageAccountName');
|
||||
const srcContainerName = requireInput('srcContainerName');
|
||||
const endpointId = requireInput('serviceConnectionARM', tl.getInput);
|
||||
const srcStorageAccountName = requireInput('srcStorageAccountName', tl.getInput);
|
||||
const dstStorageAccountName = requireInput('dstStorageAccountName', tl.getInput);
|
||||
const srcContainerName = requireInput('srcContainerName', tl.getInput);
|
||||
const dstContainerNameInput = tl.getInput('dstContainerName', false)?.trim() || '';
|
||||
const blobName = requireInput('blobName');
|
||||
const blobName = requireInput('blobName', tl.getInput);
|
||||
|
||||
const oidcBaseUrl = requireVariable('System.OidcRequestUri');
|
||||
const systemAccessToken = requireVariable('System.AccessToken');
|
||||
const oidcBaseUrl = requireVariable('System.OidcRequestUri', tl.getVariable);
|
||||
const systemAccessToken = requireVariable('System.AccessToken', tl.getVariable);
|
||||
|
||||
const metadata = getServiceConnectionMetadata(endpointId);
|
||||
const metadata = getServiceConnectionMetadata(
|
||||
endpointId,
|
||||
tl.getEndpointAuthorizationParameter,
|
||||
tl.getEndpointDataParameter
|
||||
);
|
||||
const oidcRequestUrl = buildOidcUrl(oidcBaseUrl, endpointId);
|
||||
|
||||
console.log('Requesting OIDC token for ARM authentication...');
|
||||
const oidcToken = await requestOidcToken(oidcRequestUrl, systemAccessToken);
|
||||
const oidcToken = await requestOidcToken(oidcRequestUrl, systemAccessToken, false);
|
||||
|
||||
const dstContainerName = dstContainerNameInput || srcContainerName;
|
||||
const srcUrl = buildBlobUrl(srcStorageAccountName, srcContainerName, blobName);
|
||||
const dstUrl = buildBlobUrl(dstStorageAccountName, dstContainerName, blobName);
|
||||
|
||||
console.log('Requesting storage access token from Microsoft Entra ID...');
|
||||
const accessToken = await exchangeOidcForStorageToken(metadata.tenantId, metadata.clientId, oidcToken);
|
||||
const accessToken = await exchangeOidcForScopedToken(metadata.tenantId, metadata.clientId, oidcToken, STORAGE_SCOPE);
|
||||
|
||||
console.log(`Copying blob ${srcStorageAccountName}/${srcContainerName}/${blobName} -> ${dstStorageAccountName}/${dstContainerName}/${blobName}...`);
|
||||
const copyResult = await copyBlob(srcUrl, dstUrl, accessToken);
|
||||
|
||||
Reference in New Issue
Block a user