Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions lib/utils/oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ const libaccess = require('libnpmaccess')
/**
* Handles OpenID Connect (OIDC) token retrieval and exchange for CI environments.
*
* This function is designed to work in Continuous Integration (CI) environments such as GitHub Actions
* and GitLab. It retrieves an OIDC token from the CI environment, exchanges it for an npm token, and
* This function is designed to work in Continuous Integration (CI) environments such as GitHub Actions,
* GitLab, and CircleCI. It retrieves an OIDC token from the CI environment, exchanges it for an npm token, and
* sets the token in the provided configuration for authentication with the npm registry.
*
* This function is intended to never throw, as it mutates the state of the `opts` and `config` objects on success.
* OIDC is always an optional feature, and the function should not throw if OIDC is not configured by the registry.
*
* @see https://github.com/watson/ci-info for CI environment detection.
* @see https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect for GitHub Actions OIDC.
* @see https://circleci.com/docs/openid-connect-tokens/ for CircleCI OIDC.
*/
async function oidc ({ packageName, registry, opts, config }) {
/*
Expand All @@ -29,7 +30,9 @@ async function oidc ({ packageName, registry, opts, config }) {
/** @see https://github.com/watson/ci-info/blob/v4.2.0/vendors.json#L152 */
ciInfo.GITHUB_ACTIONS ||
/** @see https://github.com/watson/ci-info/blob/v4.2.0/vendors.json#L161C13-L161C22 */
ciInfo.GITLAB
ciInfo.GITLAB ||
/** @see https://github.com/watson/ci-info/blob/v4.2.0/vendors.json#L78 */
ciInfo.CIRCLE
)) {
return undefined
}
Expand Down Expand Up @@ -143,7 +146,8 @@ async function oidc ({ packageName, registry, opts, config }) {

try {
const isDefaultProvenance = config.isDefault('provenance')
if (isDefaultProvenance) {
// CircleCI doesn't support provenance yet, so skip the auto-enable logic
if (isDefaultProvenance && !ciInfo.CIRCLE) {
const [headerB64, payloadB64] = idToken.split('.')
if (headerB64 && payloadB64) {
const payloadJson = Buffer.from(payloadB64, 'base64').toString('utf8')
Expand Down
23 changes: 22 additions & 1 deletion test/fixtures/mock-oidc.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ function githubIdToken ({ visibility = 'public' } = { visibility: 'public' }) {
return makeJwt(payload)
}

function circleciIdToken () {
const now = Math.floor(Date.now() / 1000)
const payload = {
'oidc.circleci.com/org-id': 'c9035eb6-6eb2-4c85-8a81-d9ee6a1fa8c2',
'oidc.circleci.com/project-id': 'ecc458d2-fbdc-4d9a-93c4-ac065ed3c3ca',
'oidc.circleci.com/vcs-origin': 'github.com/npm/trust-publish-test',
iat: now,
exp: now + 3600, // 1 hour expiration
}
return makeJwt(payload)
}

const mockOidc = async (t, {
oidcOptions = {},
packageName = '@npmcli/test-package',
Expand All @@ -47,6 +59,7 @@ const mockOidc = async (t, {
}) => {
const github = oidcOptions.github ?? false
const gitlab = oidcOptions.gitlab ?? false
const circleci = oidcOptions.circleci ?? false

const ACTIONS_ID_TOKEN_REQUEST_URL = oidcOptions.ACTIONS_ID_TOKEN_REQUEST_URL ?? 'https://github.com/actions/id-token'
const ACTIONS_ID_TOKEN_REQUEST_TOKEN = oidcOptions.ACTIONS_ID_TOKEN_REQUEST_TOKEN ?? 'ACTIONS_ID_TOKEN_REQUEST_TOKEN'
Expand All @@ -56,9 +69,10 @@ const mockOidc = async (t, {
env: {
ACTIONS_ID_TOKEN_REQUEST_TOKEN: ACTIONS_ID_TOKEN_REQUEST_TOKEN,
ACTIONS_ID_TOKEN_REQUEST_URL: ACTIONS_ID_TOKEN_REQUEST_URL,
CI: github || gitlab ? 'true' : undefined,
CI: github || gitlab || circleci ? 'true' : undefined,
...(github ? { GITHUB_ACTIONS: 'true' } : {}),
...(gitlab ? { GITLAB_CI: 'true' } : {}),
...(circleci ? { CIRCLECI: 'true' } : {}),
...(oidcOptions.NPM_ID_TOKEN ? { NPM_ID_TOKEN: oidcOptions.NPM_ID_TOKEN } : {}),
/* eslint-disable-next-line max-len */
...(oidcOptions.SIGSTORE_ID_TOKEN ? { SIGSTORE_ID_TOKEN: oidcOptions.SIGSTORE_ID_TOKEN } : {}),
Expand All @@ -68,17 +82,23 @@ const mockOidc = async (t, {

const GITHUB_ACTIONS = ciInfo.GITHUB_ACTIONS
const GITLAB = ciInfo.GITLAB
const CIRCLE = ciInfo.CIRCLE
delete ciInfo.GITHUB_ACTIONS
delete ciInfo.GITLAB
delete ciInfo.CIRCLE
if (github) {
ciInfo.GITHUB_ACTIONS = 'true'
}
if (gitlab) {
ciInfo.GITLAB = 'true'
}
if (circleci) {
ciInfo.CIRCLE = 'true'
}
t.teardown(() => {
ciInfo.GITHUB_ACTIONS = GITHUB_ACTIONS
ciInfo.GITLAB = GITLAB
ciInfo.CIRCLE = CIRCLE
})

const { npm, registry, joinedOutput, logs } = await loadNpmWithRegistry(t, {
Expand Down Expand Up @@ -156,6 +176,7 @@ const oidcPublishTest = (opts) => {
}

module.exports = {
circleciIdToken,
gitlabIdToken,
githubIdToken,
mockOidc,
Expand Down
31 changes: 30 additions & 1 deletion test/lib/commands/publish.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const pacote = require('pacote')
const Arborist = require('@npmcli/arborist')
const path = require('node:path')
const fs = require('node:fs')
const { githubIdToken, gitlabIdToken, oidcPublishTest, mockOidc } = require('../../fixtures/mock-oidc')
const { circleciIdToken, githubIdToken, gitlabIdToken, oidcPublishTest, mockOidc } = require('../../fixtures/mock-oidc')
const { sigstoreIdToken } = require('@npmcli/mock-registry/lib/provenance')
const mockGlobals = require('@npmcli/mock-globals')

Expand Down Expand Up @@ -1222,6 +1222,35 @@ t.test('oidc token exchange - no provenance', t => {
},
}))

t.test('circleci missing NPM_ID_TOKEN', oidcPublishTest({
oidcOptions: { circleci: true, NPM_ID_TOKEN: '' },
config: {
'//registry.npmjs.org/:_authToken': 'existing-fallback-token',
},
publishOptions: {
token: 'existing-fallback-token',
},
logsContain: [
'silly oidc Skipped because no id_token available',
],
}))

t.test('default registry success circleci', oidcPublishTest({
oidcOptions: { circleci: true, NPM_ID_TOKEN: circleciIdToken() },
config: {
'//registry.npmjs.org/:_authToken': 'existing-fallback-token',
},
mockOidcTokenExchangeOptions: {
idToken: circleciIdToken(),
body: {
token: 'exchange-token',
},
},
publishOptions: {
token: 'exchange-token',
},
}))

// custom registry success

t.test('custom registry config success github', oidcPublishTest({
Expand Down
Loading