fix(JFrogToolsInstaller): authenticate CLI download for OIDC service connections - #638
Conversation
…connections The JFrog Tools Installer built the CLI download auth via createAuthHandlers(), which only understands static credentials (access token / username+password). For an OIDC service connection none of those endpoint parameters exist, so it returned no auth handlers and downloaded the CLI anonymously — failing with HTTP 401 on a private cliInstallationRepo. It only worked when the CLI was already staged in the agent tool cache (the download was skipped entirely). Add a CLI-independent OIDC token exchange (exchangeOidcTokenViaRest) that posts the Azure DevOps ID token to JFrog Access /access/api/v1/oidc/token and uses the returned access token as a Bearer credential for the download. The existing CLI-based exchange cannot be used here because the CLI is the very artifact being downloaded (chicken-and-egg). createCliDownloadAuthHandlers() routes OIDC connections through the REST exchange and all other connection types through the existing createAuthHandlers(), so token/basic/anonymous behavior is unchanged. The handler provider is resolved lazily in getCliPath(), so a cached CLI never triggers an exchange. Adds unit tests covering the OIDC and non-OIDC CLI download auth paths.
The tests project never pinned typescript and its lockfile is not committed, so `npm i` re-resolved ts-node's unbounded `typescript: >=2.7` peer to the latest release. TypeScript 6.0.x breaks ts-node 10.9.2's config reader (TypeError: Cannot read properties of undefined (reading 'fileExists')), failing every suite before any test runs. This surfaced now (not on the last green dev run) because TS 6.0 was published after it. Pin typescript to ^5.2.2 to match the root project and keep ts-node working.
bhanurp
left a comment
There was a problem hiding this comment.
Ticket Alignment
- Implements the ticket's core ask: JFrogToolsInstaller now authenticates the CLI download itself for OIDC-based Artifactory service connections, via a new CLI-independent REST OIDC exchange (
exchangeOidcTokenViaRest) wired in throughcreateCliDownloadAuthHandlers, matching the requested "use artifactoryConnection to do the OIDC exchange to download from cliInstallationRepo" behavior. - Nothing required by the ticket appears to be missing: the fix is backward compatible (static-credential and anonymous paths untouched, download still skipped entirely when the CLI is already cached, so no exchange runs), which covers all three of the ticket's reported/working scenarios (Windows cached, OIDC + fresh Linux agent, access-token connections).
- Out of scope but reasonable/low-risk: extraction of
resolvePlatformUrl()as a shared helper (dedup with the pre-existing CLI-based OIDC flow) and atypescriptdevDependency bump for the test project.
Findings Summary
Major
jfrog-tasks-utils/utils.d.tswas not updated for this change.createCliDownloadAuthHandlersandexchangeOidcTokenViaRestare newly exported fromutils.jsbut have no type declarations, andexecuteCliTask'scliAuthHandlersparameter is still typed asifm.IRequestHandler[]even thoughtoolsInstaller.jsnow passes a() => Promise<IRequestHandler[]>provider. This isn't hypothetical: the PR's owntests/tests.tshad to bypass the type system with(jfrogUtils as any).createCliDownloadAuthHandlers(...)(see lines 142 and 166) because the declared module type doesn't have the method. Any other TypeScript consumer of@jfrog/tasks-utilsloses type safety/autocomplete for this new API and would hit the same compile error the tests worked around.
Minor
2. No test exercises the "lazy resolution" guarantee that is central to this fix's safety story (per the PR description: "a cached CLI never triggers an exchange"). The two new tests only unit-test createCliDownloadAuthHandlers in isolation; there's no test that drives getCliPath/executeCliTask with a pre-cached tool and asserts the auth-handler provider function is never invoked.
3. There's no negative-path test for exchangeOidcTokenViaRest (non-200 response or a response missing access_token) to confirm the CLI download task fails with the expected wrapped error message instead of an unhandled rejection.
Nit
4. createCliDownloadAuthHandlers and fetchOidcTokenIfConfigured now each independently re-implement the "is this an OIDC connection?" check (getEndpointAuthorizationParameter(service, 'oidcProviderName', true)). Worth factoring into a small shared isOidcConnection(service) helper alongside the already-extracted resolvePlatformUrl() so the two OIDC entry points can't drift apart later.
Overall the core fix is sound and well-targeted at the reported 401; the main gap is the stale type declarations forcing an unsafe cast in the test suite.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
||
| const statusCode = response.message.statusCode; | ||
| const responseBody = await response.readBody(); | ||
| if (statusCode !== 200) { |
There was a problem hiding this comment.
can it be 2**? can you check what are the possible status codes when successful?
There was a problem hiding this comment.
JFrog OpenAPI for /access/api/v1/oidc/token only documents 200 on success (400/401 on failure), That's the reason I kept it as 200 only.
https://docs.jfrog.com/administration/reference/oidctokenexchange
| // For OIDC service connections this performs a CLI-independent OIDC token exchange so | ||
| // the CLI download itself is authenticated; other connection types use static credentials. | ||
| let authHandlersProvider = () => utils.createCliDownloadAuthHandlers(artifactoryService); | ||
| utils.executeCliTask(RunTaskCbk, cliVersion, downloadUrl, authHandlersProvider); |
There was a problem hiding this comment.
Question: where is it downloading cli from?
There was a problem hiding this comment.
From the Artifactory service connection + the task's cliInstallationRepo input — unchanged by this PR.
let downloadUrl = utils.buildCliArtifactoryDownloadUrl(artifactoryUrl, cliInstallationRepo, cliVersion);which resolves to:
{artifactoryUrl}/{cliInstallationRepo}/{cliVersion}/jfrog-cli-{os}-{arch}/jf
e.g. https://my.jfrog.io/artifactory/jfrog-cli-remote/2.111.0/jfrog-cli-linux-amd64/jf
What this PR changes is only the auth on that download: for OIDC connections we now exchange a token first and send it as Bearer, instead of downloading anonymously (which was the 401). The URL source is the same as before.
| return Promise.resolve(typeof cliAuthHandlers === 'function' ? cliAuthHandlers() : cliAuthHandlers) | ||
| .then((resolvedHandlers) => downloadCli(cliDownloadUrl, resolvedHandlers, cliVersion)) |
There was a problem hiding this comment.
this type of checks in javascript are little risky, I would look for some other mechanism where I can maintain the consistency of the data type instead of passing a function or an array.
Maybe write a factory method or a strategy pattern returning a resolver function which does the needful
There was a problem hiding this comment.
Current dual type keeps backward compat for the other 17 tasks that still pass an array.
A factory would be a broader refactor, if you still think if that's a valid case do let me know, I can again look into it.
There was a problem hiding this comment.
let's pass and object (struct) then, and pass array in another variable, just like other 17 are doing and maybe pass function as another field in the struct from the new method. Check is one of them is null then go ahead with other variable, will this work?
| "sync-request": "^6.1.0", | ||
| "ts-node": "^10.9.1" | ||
| "ts-node": "^10.9.1", | ||
| "typescript": "^5.2.2" |
There was a problem hiding this comment.
I don't think this dependency is required
There was a problem hiding this comment.
It was pinned because unpinned ts-node peer resolved to TypeScript 6.x and broke every suite (fileExists error). Pin to ^5.2.2 matches root and keeps tests runnable.
There was a problem hiding this comment.
yeah I guess that is because in your local you might be on another node version
Co-authored-by: Cursor <cursoragent@cursor.com>
…lution Add unit tests for the two pieces of new logic that previously had no direct coverage: - exchangeOidcTokenViaRest: exercises the real function with a stubbed HTTP client — asserts the request shape (endpoint, grant_type, subject_token_type, subject_token, provider_name, provider_type=Azure, audience), that it returns the exchanged access token and publishes the oidc_token output variable, and that it throws clear errors on a non-200 response and on a missing access_token. - getCliPath auth-handler resolution: a function provider is resolved to an array before reaching toolLib.downloadTool (guards the "this.handlers.forEach is not a function" regression), and a plain array handler still passes through unchanged (backward compatibility).
Brings the post-2026-04-30 non-e2e product work onto v2 so the release pipeline can build and publish the new version: - #638: authenticate JFrog CLI download for OIDC service connections (JFrogToolsInstaller) - Default JFrog CLI 2.111.0 / pluginVersion 2.14.2 - Node 22 in the test matrix (RTECO-813) Also aligns e2e-plugin-tests.yml and trigger-ado-pipeline.sh with dev so the release step `git merge origin/dev` is conflict-free. No change to extension task runtime behavior beyond the items above.
Problem
The JFrog Tools Installer task fails to download the JFrog CLI with HTTP 401 when the Artifactory service connection uses OIDC, if the CLI is not already staged in the agent tool cache:
It works with access-token connections, and it works on agents where the CLI is already cached (the download is skipped entirely), which is why the failure only surfaces for OIDC + a fresh agent.
Root cause
JFrogToolsInstallerbuilt the CLI-download authentication viacreateAuthHandlers(), which only understands static credentials:apitoken→ Bearer handlerusername+password→ Basic handler[](anonymous)An OIDC service connection carries none of those endpoint parameters — its credential only exists after an OIDC token exchange. So
createAuthHandlers()returned[], the CLI was downloaded anonymously, and a privatecliInstallationReporesponded 401.The extension's existing OIDC exchange runs
jf eot, i.e. it needs the CLI to already exist — which is a chicken-and-egg problem at install time, since the CLI is the very artifact being downloaded.Fix
exchangeOidcTokenViaRest()— a CLI-independent OIDC exchange. Reuses the existingfetchAzureOidcToken()for the Azure DevOps ID token, thenPOSTs it to JFrog Access/access/api/v1/oidc/tokenwith the same requestjf eotsends for an Azure provider (grant_type/subject_token_type/subject_token/provider_name/provider_type=Azure/audience), and returns the exchanged access token.createCliDownloadAuthHandlers()— routes OIDC connections through the REST exchange (Bearer of the exchanged token) and every other connection type through the existingcreateAuthHandlers()(unchanged).getCliPath()only invokes it when a download is actually required, so a cached CLI never triggers an exchange.resolvePlatformUrl()helper (dedup with the existing OIDC flow).Backward compatibility
The only shared code path changed is
getCliPath()(used by all 18 tasks); it accepts an array or a provider function, and arrays pass through untouched. Verified behavior across scenarios:[]handlers — unchangedBearer(apitoken), no exchangeBearer(exchanged token)authenticates the download (the fix)Token, basic, anonymous, cached, custom-path, releases-fallback and the extractors flow are all unchanged.
tscandeslintare clean on the changed files.Tests
Adds unit tests covering the OIDC download path (Bearer built from the exchanged token) and the non-OIDC path (static credentials, no exchange).