Skip to content

Commit 12dd778

Browse files
feat: Adding oidc implementation for service connections.
1 parent c003470 commit 12dd778

File tree

7 files changed

+596
-233
lines changed

7 files changed

+596
-233
lines changed

README.md

Lines changed: 390 additions & 227 deletions
Large diffs are not rendered by default.

images/oidc-integration.png

51.1 KB
Loading

images/oidc-json-mapping.png

12.5 KB
Loading

images/oidc-service-connection.png

104 KB
Loading

jfrog-tasks-utils/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"azure-pipelines-task-lib": "4.5.0",
1212
"azure-pipelines-tool-lib": "2.0.6",
1313
"azure-pipelines-tasks-java-common": "^2.219.1",
14-
"typed-rest-client": "^1.8.11"
14+
"typed-rest-client": "^1.8.11",
15+
"sync-request": "^6.1.0"
1516
},
1617
"scripts": {
1718
"test": "echo \"Error: no test specified\" && exit 1"

jfrog-tasks-utils/utils.js

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const buildAgent = 'jfrog-azure-devops-extension';
1919
const customFolderPath = encodePath(join(jfrogFolderPath, 'current'));
2020
const customCliPath = encodePath(join(customFolderPath, fileName)); // Optional - Customized jfrog-cli path.
2121
const jfrogCliReleasesUrl = 'https://releases.jfrog.io/artifactory/jfrog-cli/v2-jf';
22-
22+
const request = require('sync-request');
2323
// Set by Tools Installer Task. This JFrog CLI version will be used in all tasks unless manual installation is used,
2424
// or a specific version was requested in a task. If not set, use the default CLI version.
2525
const pipelineRequestedCliVersionEnv = 'JFROG_CLI_PIPELINE_REQUESTED_VERSION_AZURE';
@@ -252,15 +252,90 @@ function configureDistributionCliServer(distributionService, serverId, cliPath,
252252
function configureXrayCliServer(xrayService, serverId, cliPath, buildDir) {
253253
return configureSpecificCliServer(xrayService, '--xray-url', serverId, cliPath, buildDir);
254254
}
255+
function logIDToken(oidcToken) {
256+
const oidcClaims = JSON.parse(Buffer.from(oidcToken.split('.')[1], 'base64').toString());
257+
console.log('OIDC Token Subject: ', oidcClaims.sub);
258+
console.log(`OIDC Token Claims: {"sub": "${oidcClaims.sub}"}`);
259+
console.log('OIDC Token Issuer (Provider URL): ', oidcClaims.iss);
260+
console.log('OIDC Token Audience: ', oidcClaims.aud);
261+
}
262+
263+
function getADOIdToken(serviceConnectionID) {
264+
const uri = tl.getVariable('System.CollectionUri');
265+
const teamPrjID = tl.getVariable('System.TeamProjectId');
266+
const hub = tl.getVariable('System.HostType');
267+
const planID = tl.getVariable('System.PlanId');
268+
const jobID = tl.getVariable('System.JobId');
269+
const apiVersion = '7.1-preview.1';
270+
271+
const url = `${uri}${teamPrjID}/_apis/distributedtask/hubs/${hub}/plans/${planID}/jobs/${jobID}/oidctoken?api-version=${apiVersion}&serviceConnectionId=${serviceConnectionID}`;
272+
273+
try {
274+
const response = request('POST', url, {
275+
headers: {
276+
'Content-Type': 'application/json',
277+
Authorization: `Bearer ${tl.getVariable('System.AccessToken')}`,
278+
},
279+
});
280+
281+
if (response.statusCode !== 200) {
282+
throw new Error(`HTTP request failed with status code ${response.statusCode}`);
283+
}
284+
285+
const parsedResponse = JSON.parse(response.getBody('utf8'));
286+
const idToken = parsedResponse.oidcToken;
287+
logIDToken(idToken);
288+
return idToken;
289+
} catch (error) {
290+
throw new Error(`Failed to get or parse response: ${error.message}`);
291+
}
292+
}
293+
294+
function getArtifactoryAccessToken(adoJWT, oidcProviderName, jfrogPlatformUrl) {
295+
const payload = {
296+
grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',
297+
subject_token_type: 'urn:ietf:params:oauth:token-type:id_token',
298+
subject_token: adoJWT,
299+
provider_name: oidcProviderName,
300+
};
301+
302+
const url = `${jfrogPlatformUrl}/access/api/v1/oidc/token`;
303+
304+
try {
305+
const response = request('POST', url, {
306+
headers: {
307+
'Content-Type': 'application/json',
308+
},
309+
json: payload,
310+
});
311+
312+
if (response.statusCode !== 200) {
313+
throw new Error(`HTTP request failed with status code ${response.statusCode}: ${response.getBody('utf8')}`);
314+
}
315+
316+
const parsedResponse = JSON.parse(response.getBody('utf8'));
317+
return parsedResponse.access_token;
318+
} catch (error) {
319+
throw new Error(`Failed to get or parse response: ${error.message}`);
320+
}
321+
}
255322

256323
function configureSpecificCliServer(service, urlFlag, serverId, cliPath, buildDir) {
257324
let serviceUrl = tl.getEndpointUrl(service, false);
258325
let serviceUser = tl.getEndpointAuthorizationParameter(service, 'username', true);
259326
let servicePassword = tl.getEndpointAuthorizationParameter(service, 'password', true);
260327
let serviceAccessToken = tl.getEndpointAuthorizationParameter(service, 'apitoken', true);
328+
let oidcProviderName = tl.getEndpointAuthorizationParameter(service, 'oidcProviderName', true);
329+
let jfrogPlatformUrl = tl.getEndpointAuthorizationParameter(service, 'jfrogPlatformUrl', true);
261330
let cliCommand = cliJoin(cliPath, jfrogCliConfigAddCommand, quote(serverId), urlFlag + '=' + quote(serviceUrl), '--interactive=false');
262331
let stdinSecret;
263332
let secretInStdinSupported = isStdinSecretSupported();
333+
334+
if (oidcProviderName) {
335+
const idToken = getADOIdToken(service);
336+
serviceAccessToken = getArtifactoryAccessToken(idToken, oidcProviderName, jfrogPlatformUrl);
337+
}
338+
264339
if (serviceAccessToken) {
265340
// Add access-token if required.
266341
cliCommand = cliJoin(cliCommand, secretInStdinSupported ? '--access-token-stdin' : '--access-token=' + quote(serviceAccessToken));

vss-extension.json

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,37 @@
149149
}
150150
}
151151
]
152+
},
153+
{
154+
"type": "ms.vss-endpoint.endpoint-auth-scheme-none",
155+
"displayName": "OpenID Connect Integration",
156+
"properties": {
157+
"isVerifiable": "False"
158+
},
159+
"inputDescriptors": [
160+
{
161+
"id": "oidcProviderName",
162+
"name": "OpenID Connect Provider Name",
163+
"description": "The OpenID Connect \"Provider Name\" configured in JFrog Platform. Click <a href=\"https://github.com/jfrog/jfrog-azure-devops-extension/tree/v2?tab=readme-ov-file#using-openid-connect-oidc-authentication\" target=\"_blank\">here</a> for information about how to configure OpenID Connect.",
164+
"inputMode": "textbox",
165+
"isConfidential": false,
166+
"validation": {
167+
"isRequired": true,
168+
"dataType": "string"
169+
}
170+
},
171+
{
172+
"id": "jfrogPlatformUrl",
173+
"name": "Platform URL",
174+
"description": "The access token will be obtained from this URL. For example, https://my.jfrog.io/",
175+
"inputMode": "textbox",
176+
"isConfidential": false,
177+
"validation": {
178+
"isRequired": true,
179+
"dataType": "string"
180+
}
181+
}
182+
]
152183
}
153184
]
154185
}
@@ -165,7 +196,7 @@
165196
"displayName": "JFrog Artifactory V2",
166197
"url": {
167198
"displayName": "Server URL",
168-
"helpText": "Specify the root URL of your Artifactory installation. For example, https://repo.jfrog.org/artifactory"
199+
"helpText": "Specify the root URL of your Artifactory installation. For example, https://my.jfrog.io/artifactory"
169200
},
170201
"icon": "images/artifactory.png",
171202
"dataSources": [
@@ -234,6 +265,37 @@
234265
}
235266
}
236267
]
268+
},
269+
{
270+
"type": "ms.vss-endpoint.endpoint-auth-scheme-none",
271+
"displayName": "OpenID Connect Integration",
272+
"properties": {
273+
"isVerifiable": "False"
274+
},
275+
"inputDescriptors": [
276+
{
277+
"id": "oidcProviderName",
278+
"name": "OpenID Connect Provider Name",
279+
"description": "The OpenID Connect \"Provider Name\" configured in JFrog Platform. Click <a href=\"https://github.com/jfrog/jfrog-azure-devops-extension/tree/v2?tab=readme-ov-file#using-openid-connect-oidc-authentication\" target=\"_blank\">here</a> for information about how to configure OpenID Connect.",
280+
"inputMode": "textbox",
281+
"isConfidential": false,
282+
"validation": {
283+
"isRequired": true,
284+
"dataType": "string"
285+
}
286+
},
287+
{
288+
"id": "jfrogPlatformUrl",
289+
"name": "Platform URL",
290+
"description": "The access token will be obtained from this URL. For example, https://my.jfrog.io/",
291+
"inputMode": "textbox",
292+
"isConfidential": false,
293+
"validation": {
294+
"isRequired": true,
295+
"dataType": "string"
296+
}
297+
}
298+
]
237299
}
238300
]
239301
}
@@ -250,7 +312,7 @@
250312
"displayName": "JFrog Distribution V2",
251313
"url": {
252314
"displayName": "Server URL",
253-
"helpText": "Specify the root URL of your Distribution installation. For example, https://repo.jfrog.org/distribution"
315+
"helpText": "Specify the root URL of your Distribution installation. For example, https://my.jfrog.io/distribution"
254316
},
255317
"icon": "images/distribution.png",
256318
"dataSources": [
@@ -309,6 +371,37 @@
309371
}
310372
}
311373
]
374+
},
375+
{
376+
"type": "ms.vss-endpoint.endpoint-auth-scheme-none",
377+
"displayName": "OpenID Connect Integration",
378+
"properties": {
379+
"isVerifiable": "False"
380+
},
381+
"inputDescriptors": [
382+
{
383+
"id": "oidcProviderName",
384+
"name": "OpenID Connect Provider Name",
385+
"description": "The OpenID Connect \"Provider Name\" configured in JFrog Platform. Click <a href=\"https://github.com/jfrog/jfrog-azure-devops-extension/tree/v2?tab=readme-ov-file#using-openid-connect-oidc-authentication\" target=\"_blank\">here</a> for information about how to configure OpenID Connect.",
386+
"inputMode": "textbox",
387+
"isConfidential": false,
388+
"validation": {
389+
"isRequired": true,
390+
"dataType": "string"
391+
}
392+
},
393+
{
394+
"id": "jfrogPlatformUrl",
395+
"name": "Platform URL",
396+
"description": "The access token will be obtained from this URL. For example, https://my.jfrog.io/",
397+
"inputMode": "textbox",
398+
"isConfidential": false,
399+
"validation": {
400+
"isRequired": true,
401+
"dataType": "string"
402+
}
403+
}
404+
]
312405
}
313406
]
314407
}
@@ -325,7 +418,7 @@
325418
"displayName": "JFrog Xray V2",
326419
"url": {
327420
"displayName": "Server URL",
328-
"helpText": "Specify the root URL of your Xray installation. For example, https://repo.jfrog.org/xray"
421+
"helpText": "Specify the root URL of your Xray installation. For example, https://my.jfrog.io/xray"
329422
},
330423
"icon": "images/xray.png",
331424
"dataSources": [
@@ -384,6 +477,37 @@
384477
}
385478
}
386479
]
480+
},
481+
{
482+
"type": "ms.vss-endpoint.endpoint-auth-scheme-none",
483+
"displayName": "OpenID Connect Integration",
484+
"properties": {
485+
"isVerifiable": "False"
486+
},
487+
"inputDescriptors": [
488+
{
489+
"id": "oidcProviderName",
490+
"name": "OpenID Connect Provider Name",
491+
"description": "The OpenID Connect \"Provider Name\" configured in JFrog Platform. Click <a href=\"https://github.com/jfrog/jfrog-azure-devops-extension/tree/v2?tab=readme-ov-file#using-openid-connect-oidc-authentication\" target=\"_blank\">here</a> for information about how to configure OpenID Connect.",
492+
"inputMode": "textbox",
493+
"isConfidential": false,
494+
"validation": {
495+
"isRequired": true,
496+
"dataType": "string"
497+
}
498+
},
499+
{
500+
"id": "jfrogPlatformUrl",
501+
"name": "Platform URL",
502+
"description": "The access token will be obtained from this URL. For example, https://my.jfrog.io/",
503+
"inputMode": "textbox",
504+
"isConfidential": false,
505+
"validation": {
506+
"isRequired": true,
507+
"dataType": "string"
508+
}
509+
}
510+
]
387511
}
388512
]
389513
}
@@ -908,4 +1032,4 @@
9081032
"path": "tasks/JFrogGenericArtifacts"
9091033
}
9101034
]
911-
}
1035+
}

0 commit comments

Comments
 (0)