diff --git a/.github/workflows/oidc-integration-test.yml b/.github/workflows/oidc-integration-test.yml index 172673834..099767e6c 100644 --- a/.github/workflows/oidc-integration-test.yml +++ b/.github/workflows/oidc-integration-test.yml @@ -5,7 +5,6 @@ name: OIDC Integration Test # - 2.74.1: Does not support `jf eot` command, validates manual fallback logic. # - 2.75.0: Introduced native OIDC token exchange. # - Latest: Ensures ongoing compatibility with the most recent CLI build. - on: push: branches: @@ -23,31 +22,27 @@ permissions: contents: read jobs: - oidc-test: + generate-platform-oidc-integration: strategy: - fail-fast: false + # Using "include" instead of a matrix of arrays gives us fine-grained control over test combinations. + # This is needed because some audience values (e.g., URLs) contain characters not valid in matrix keys or job names. + # + # Each scenario represents a real-world case: + # - "default": No audience is set in the action or the platform integration. + # - "test": A custom audience is explicitly set in both the action and the platform integration. + # - "github-implicit-default": The platform integration is explicitly configured with GitHub's default audience, + # but the action does not pass any audience. + # This tests CLI behavior in case of mismatches — see https://github.com/jfrog/setup-jfrog-cli/issues/270 matrix: - os: [ ubuntu, macos, windows ] - cli-version: [ '2.74.1', '2.75.0','latest' ] - runs-on: ${{ matrix.os }}-latest - name: OIDC Test - ${{ matrix.cli-version }} on ${{ matrix.os }} - env: - JFROG_CLI_LOG_LEVEL: DEBUG - + include: + - audience_id: default + audience_value: '' + - audience_id: test + audience_value: 'audience-value' + - audience_id: github-implicit-default + audience_value: 'https://github.com/jfrog' + runs-on: ubuntu-latest steps: - - name: Checkout Repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.pull_request.head.sha }} - - # Setup OIDC platform integration - - name: Generate unique OIDC provider name - id: gen-oidc - shell: bash - run: | - cli_version="${{ matrix.cli-version }}" && cli_version="${cli_version//./-}" - echo "oidc_provider_name=oidc-integration-${cli_version}-${{ matrix.os }}-$(date +%s)" >> "$GITHUB_OUTPUT" - - name: Create OpenID Connect integration shell: bash run: | @@ -55,22 +50,23 @@ jobs: -H "Content-Type: application/json" \ -H "Authorization: Bearer ${{ secrets.JFROG_PLATFORM_RT_TOKEN }}" \ -d '{ - "name": "${{ steps.gen-oidc.outputs.oidc_provider_name }}", + "name": "oidc-integration-${{ matrix.audience_id }}-${{ github.run_id }}", "issuer_url": "https://token.actions.githubusercontent.com", "provider_type": "GitHub", - "enable_permissive_configuration": "true", - "description": "Test configuration for CLI version ${{ matrix.cli-version }}" + "audience": "${{ matrix.audience_value }}", + "enable_permissive_configuration": true, + "description": "Temp integration for testing OIDC with audience value: ${{ matrix.audience_value }}" }' - name: Create OIDC Identity Mapping shell: bash run: | - curl -X POST "${{ secrets.JFROG_PLATFORM_URL }}/access/api/v1/oidc/${{ steps.gen-oidc.outputs.oidc_provider_name }}/identity_mappings" \ - -H 'Content-Type: application/json' \ + curl -X POST "${{ secrets.JFROG_PLATFORM_URL }}/access/api/v1/oidc/oidc-integration-${{ matrix.audience_id }}-${{ github.run_id }}/identity_mappings" \ + -H "Content-Type: application/json" \ -H "Authorization: Bearer ${{ secrets.JFROG_PLATFORM_RT_TOKEN }}" \ -d '{ "name": "oidc-test-mapping", - "priority": "1", + "priority": 1, "claims": { "repository": "${{ github.repository_owner }}/setup-jfrog-cli" }, @@ -80,7 +76,52 @@ jobs: } }' - # Setup + oidc-test: + needs: generate-platform-oidc-integration + strategy: + fail-fast: false + # Using include allows exact combinations of CLI version and audience ID to ensure coverage of edge cases. + # This avoids invalid audience strings in identifiers and ensures fallback logic is tested selectively. + matrix: + include: + - cli-version: '2.74.1' + audience_id: default + audience_value: '' + - cli-version: '2.75.0' + audience_id: default + audience_value: '' + - cli-version: latest + audience_id: default + audience_value: '' + - cli-version: '2.74.1' + audience_id: test + audience_value: 'audience-value' + - cli-version: '2.75.0' + audience_id: test + audience_value: 'audience-value' + - cli-version: latest + audience_id: test + audience_value: 'audience-value' + # GitHub default audience value is resolved implicitly when omitted. + # These tests verify that the CLI handles an empty value correctly while GitHub sets the expected audience on its backend. + - cli-version: '2.74.1' + audience_id: github-implicit-default + audience_value: '' + - cli-version: '2.75.0' + audience_id: github-implicit-default + audience_value: '' + - cli-version: latest + audience_id: github-implicit-default + audience_value: '' + runs-on: ubuntu-latest + env: + JFROG_CLI_LOG_LEVEL: DEBUG + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Setup JFrog CLI id: setup-jfrog-cli uses: ./ @@ -88,13 +129,12 @@ jobs: JF_URL: ${{ secrets.JFROG_PLATFORM_URL }} with: version: ${{ matrix.cli-version }} - oidc-provider-name: ${{ steps.gen-oidc.outputs.oidc_provider_name }} + oidc-provider-name: oidc-integration-${{ matrix.audience_id }}-${{ github.run_id }} + oidc-audience: ${{ matrix.audience_value }} - # validate successful OIDC configuration - name: Test JFrog CLI connectivity run: jf rt ping - # Validate step outputs - name: Validate user output shell: bash run: test -n "${{ steps.setup-jfrog-cli.outputs.oidc-user }}" @@ -103,10 +143,19 @@ jobs: shell: bash run: test -n "${{ steps.setup-jfrog-cli.outputs.oidc-token }}" - # Cleanup + cleanup-oidc-integration: + needs: oidc-test + if: always() + strategy: + matrix: + include: + - audience_id: default + - audience_id: test + - audience_id: github-implicit-default + runs-on: ubuntu-latest + steps: - name: Delete OIDC integration shell: bash - if: always() run: | - curl -X DELETE "${{ secrets.JFROG_PLATFORM_URL }}/access/api/v1/oidc/${{ steps.gen-oidc.outputs.oidc_provider_name }}" \ + curl -X DELETE "${{ secrets.JFROG_PLATFORM_URL }}/access/api/v1/oidc/oidc-integration-${{ matrix.audience_id }}-${{ github.run_id }}" \ -H "Authorization: Bearer ${{ secrets.JFROG_PLATFORM_RT_TOKEN }}" diff --git a/lib/oidc-utils.js b/lib/oidc-utils.js index b5e5db15c..845fcebbc 100644 --- a/lib/oidc-utils.js +++ b/lib/oidc-utils.js @@ -64,7 +64,13 @@ class OidcUtils { throw new Error(`JF_URL must be provided when oidc-provider-name is specified`); } // Get OIDC token ID from GitHub - jfrogCredentials.oidcTokenId = yield this.getIdToken(jfrogCredentials.oidcAudience || ''); + try { + core.debug('Attempting to fetch JSON Web Token (JWT) ID token with audience value: ' + jfrogCredentials.oidcAudience); + jfrogCredentials.oidcTokenId = yield core.getIDToken(jfrogCredentials.oidcAudience); + } + catch (error) { + throw new Error(`Failed to fetch OpenID Connect JSON Web Token: ${error.message}`); + } // Version should be more than min version // If CLI_REMOTE_ARG specified, we have to fetch token before we can download the CLI. if (this.isCLIVersionOidcSupported() && !core.getInput(utils_1.Utils.CLI_REMOTE_ARG)) { @@ -90,7 +96,12 @@ class OidcUtils { if (creds.oidcProviderName === undefined || creds.oidcTokenId === undefined || creds.jfrogUrl === undefined) { throw new Error('Missing one or more required fields: OIDC provider name, token ID, or JFrog Platform URL.'); } - output = yield utils_1.Utils.runCliAndGetOutput(['eot', creds.oidcProviderName, creds.oidcTokenId, '--url', creds.jfrogUrl, '--oidc-audience', creds.oidcAudience || 'jfrog-github'], { silent: true }); + const args = ['eot', creds.oidcProviderName, creds.oidcTokenId, '--url', creds.jfrogUrl]; + if (creds.oidcAudience !== "") { + args.push('--oidc-audience', creds.oidcAudience); + } + core.debug('Running CLI command: ' + args.join(' ')); + output = yield utils_1.Utils.runCliAndGetOutput(args, { silent: true }); const { accessToken, username } = this.extractValuesFromOIDCToken(output); this.setOidcStepOutputs(username, accessToken); return accessToken; @@ -284,23 +295,6 @@ class OidcUtils { return yield fs_1.promises.readFile(configRelativePath, 'utf-8'); }); } - /** - * Fetches a JSON Web Token (JWT) ID token from GitHub's OIDC provider. - * @param audience - The intended audience for the token. - * @returns A promise that resolves to the JWT ID token as a string. - * @throws An error if fetching the token fails. - */ - static getIdToken(audience) { - return __awaiter(this, void 0, void 0, function* () { - core.debug('Attempting to fetch JSON Web Token (JWT) ID token...'); - try { - return yield core.getIDToken(audience); - } - catch (error) { - throw new Error(`Failed to fetch OpenID Connect JSON Web Token: ${error.message}`); - } - }); - } static isCLIVersionOidcSupported() { const version = core.getInput(utils_1.Utils.CLI_VERSION_ARG) || ''; if (version === '') { diff --git a/lib/utils.js b/lib/utils.js index 0356c1588..c9673c73c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -55,7 +55,7 @@ class Utils { username: process.env.JF_USER, password: process.env.JF_PASSWORD, oidcProviderName: core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME), - oidcAudience: core.getInput(Utils.OIDC_AUDIENCE_ARG) || Utils.DEFAULT_OIDC_AUDIENCE, + oidcAudience: core.getInput(Utils.OIDC_AUDIENCE_ARG) || '', oidcTokenId: '', }; if (jfrogCredentials.password && !jfrogCredentials.username) { @@ -184,7 +184,6 @@ class Utils { * @name password - JFrog Platform basic authentication * @name accessToken - Jfrog Platform access token * @name oidcProviderName - OpenID Connect provider name defined in the JFrog Platform - * @name oidcAudience - JFrog Platform OpenID Connect audience */ let url = jfrogCredentials.jfrogUrl; let user = jfrogCredentials.username; @@ -474,4 +473,3 @@ Utils.JOB_SUMMARY_DISABLE = 'disable-job-summary'; Utils.AUTO_BUILD_PUBLISH_DISABLE = 'disable-auto-build-publish'; // Custom server ID input Utils.CUSTOM_SERVER_ID = 'custom-server-id'; -Utils.DEFAULT_OIDC_AUDIENCE = 'jfrog-github'; diff --git a/src/oidc-utils.ts b/src/oidc-utils.ts index 67c5491ea..634c802ad 100644 --- a/src/oidc-utils.ts +++ b/src/oidc-utils.ts @@ -41,7 +41,12 @@ export class OidcUtils { throw new Error(`JF_URL must be provided when oidc-provider-name is specified`); } // Get OIDC token ID from GitHub - jfrogCredentials.oidcTokenId = await this.getIdToken(jfrogCredentials.oidcAudience || ''); + try { + core.debug('Attempting to fetch JSON Web Token (JWT) ID token with audience value: ' + jfrogCredentials.oidcAudience); + jfrogCredentials.oidcTokenId = await core.getIDToken(jfrogCredentials.oidcAudience); + } catch (error: any) { + throw new Error(`Failed to fetch OpenID Connect JSON Web Token: ${error.message}`); + } // Version should be more than min version // If CLI_REMOTE_ARG specified, we have to fetch token before we can download the CLI. @@ -69,10 +74,12 @@ export class OidcUtils { throw new Error('Missing one or more required fields: OIDC provider name, token ID, or JFrog Platform URL.'); } - output = await Utils.runCliAndGetOutput( - ['eot', creds.oidcProviderName, creds.oidcTokenId, '--url', creds.jfrogUrl, '--oidc-audience', creds.oidcAudience || 'jfrog-github'], - { silent: true }, - ); + const args = ['eot', creds.oidcProviderName, creds.oidcTokenId, '--url', creds.jfrogUrl]; + if (creds.oidcAudience !== "") { + args.push('--oidc-audience', creds.oidcAudience); + } + core.debug('Running CLI command: ' + args.join(' ')); + output = await Utils.runCliAndGetOutput(args, { silent: true }); const { accessToken, username }: CliExchangeTokenResponse = this.extractValuesFromOIDCToken(output); this.setOidcStepOutputs(username, accessToken); @@ -276,21 +283,6 @@ export class OidcUtils { return await fs.readFile(configRelativePath, 'utf-8'); } - /** - * Fetches a JSON Web Token (JWT) ID token from GitHub's OIDC provider. - * @param audience - The intended audience for the token. - * @returns A promise that resolves to the JWT ID token as a string. - * @throws An error if fetching the token fails. - */ - private static async getIdToken(audience: string): Promise { - core.debug('Attempting to fetch JSON Web Token (JWT) ID token...'); - try { - return await core.getIDToken(audience); - } catch (error: any) { - throw new Error(`Failed to fetch OpenID Connect JSON Web Token: ${error.message}`); - } - } - public static isCLIVersionOidcSupported(): boolean { const version: string = core.getInput(Utils.CLI_VERSION_ARG) || ''; if (version === '') { diff --git a/src/types.ts b/src/types.ts index e998432fb..ff5c5524c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,7 +12,7 @@ export interface JfrogCredentials { accessToken?: string; oidcProviderName?: string; oidcTokenId?: string; - oidcAudience?: string; + oidcAudience : string; } /** diff --git a/src/utils.ts b/src/utils.ts index 32de63c85..a2bf08c6f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -52,7 +52,6 @@ export class Utils { public static readonly AUTO_BUILD_PUBLISH_DISABLE: string = 'disable-auto-build-publish'; // Custom server ID input private static readonly CUSTOM_SERVER_ID: string = 'custom-server-id'; - private static DEFAULT_OIDC_AUDIENCE: string = 'jfrog-github'; /** * Gathers JFrog's credentials from environment variables and delivers them in a JfrogCredentials structure @@ -66,7 +65,7 @@ export class Utils { username: process.env.JF_USER, password: process.env.JF_PASSWORD, oidcProviderName: core.getInput(Utils.OIDC_INTEGRATION_PROVIDER_NAME), - oidcAudience: core.getInput(Utils.OIDC_AUDIENCE_ARG) || Utils.DEFAULT_OIDC_AUDIENCE, + oidcAudience: core.getInput(Utils.OIDC_AUDIENCE_ARG) || '', oidcTokenId: '', } as JfrogCredentials; @@ -204,7 +203,6 @@ export class Utils { * @name password - JFrog Platform basic authentication * @name accessToken - Jfrog Platform access token * @name oidcProviderName - OpenID Connect provider name defined in the JFrog Platform - * @name oidcAudience - JFrog Platform OpenID Connect audience */ let url: string | undefined = jfrogCredentials.jfrogUrl; let user: string | undefined = jfrogCredentials.username; diff --git a/test/main.spec.ts b/test/main.spec.ts index d24575841..7fbbb1b8e 100644 --- a/test/main.spec.ts +++ b/test/main.spec.ts @@ -111,6 +111,24 @@ describe('Collect JFrog Credentials from env vars exceptions', () => { process.env['JF_PASSWORD'] = password; expect(() => Utils.collectJfrogCredentialsFromEnvVars()).toThrow(new Error(exception)); }); + + test('collectJfrogCredentialsFromEnvVars should return default values when no environment variables are set', () => { + // Ensure no relevant environment variables are set + delete process.env['JF_URL']; + delete process.env['JF_ACCESS_TOKEN']; + delete process.env['JF_USER']; + delete process.env['JF_PASSWORD']; + + // Call the function + const jfrogCredentials: JfrogCredentials = Utils.collectJfrogCredentialsFromEnvVars(); + + // Verify default values + expect(jfrogCredentials.jfrogUrl).toBeUndefined(); + expect(jfrogCredentials.accessToken).toBeUndefined(); + expect(jfrogCredentials.username).toBeUndefined(); + expect(jfrogCredentials.password).toBeUndefined(); + expect(jfrogCredentials.oidcAudience).toEqual("") + }); }); async function testConfigCommand(expectedServerId: string) { diff --git a/test/oidc-utils.spec.ts b/test/oidc-utils.spec.ts index 027d0b876..d04da5491 100644 --- a/test/oidc-utils.spec.ts +++ b/test/oidc-utils.spec.ts @@ -88,6 +88,7 @@ describe('OidcUtils', (): void => { it('should throw if creds are missing required fields', async (): Promise => { const incompleteCreds: JfrogCredentials = { jfrogUrl: 'https://example.jfrog.io', + oidcAudience: '' // missing provider and token ID };