UNPKG

npm

Version:

a package manager for JavaScript

177 lines (158 loc) 7.1 kB
const { log } = require('proc-log') const npmFetch = require('npm-registry-fetch') const ciInfo = require('ci-info') const fetch = require('make-fetch-happen') const npa = require('npm-package-arg') 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 * 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. */ async function oidc ({ packageName, registry, opts, config }) { /* * This code should never run when people try to publish locally on their machines. * It is designed to execute only in Continuous Integration (CI) environments. */ try { if (!( /** @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 )) { return undefined } /** * Check if the environment variable `NPM_ID_TOKEN` is set. * In GitLab CI, the ID token is provided via an environment variable, * with `NPM_ID_TOKEN` serving as a predefined default. For consistency, * all supported CI environments are expected to support this variable. * In contrast, GitHub Actions uses a request-based approach to retrieve the ID token. * The presence of this token within GitHub Actions will override the request-based approach. * This variable follows the prefix/suffix convention from sigstore (e.g., `SIGSTORE_ID_TOKEN`). * @see https://docs.sigstore.dev/cosign/signing/overview/ */ let idToken = process.env.NPM_ID_TOKEN if (!idToken && ciInfo.GITHUB_ACTIONS) { /** * GitHub Actions provides these environment variables: * - `ACTIONS_ID_TOKEN_REQUEST_URL`: The URL to request the ID token. * - `ACTIONS_ID_TOKEN_REQUEST_TOKEN`: The token to authenticate the request. * Only when a workflow has the following permissions: * ``` * permissions: * id-token: write * ``` * @see https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings */ if (!( process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN )) { log.silly('oidc', 'Skipped because incorrect permissions for id-token within GitHub workflow') return undefined } /** * The specification for an audience is `npm:registry.npmjs.org`, * where "registry.npmjs.org" can be any supported registry. */ const audience = `npm:${new URL(registry).hostname}` const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL) url.searchParams.append('audience', audience) const startTime = Date.now() const response = await fetch(url.href, { retry: opts.retry, headers: { Accept: 'application/json', Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`, }, }) const elapsedTime = Date.now() - startTime log.http( 'fetch', `GET ${url.href} ${response.status} ${elapsedTime}ms` ) const json = await response.json() if (!response.ok) { log.verbose('oidc', `Failed to fetch id_token from GitHub: received an invalid response`) return undefined } if (!json.value) { log.verbose('oidc', `Failed to fetch id_token from GitHub: missing value`) return undefined } idToken = json.value } if (!idToken) { log.silly('oidc', 'Skipped because no id_token available') return undefined } const parsedRegistry = new URL(registry) const regKey = `//${parsedRegistry.host}${parsedRegistry.pathname}` const authTokenKey = `${regKey}:_authToken` const escapedPackageName = npa(packageName).escapedName let response try { response = await npmFetch.json(new URL(`/-/npm/v1/oidc/token/exchange/package/${escapedPackageName}`, registry), { ...opts, [authTokenKey]: idToken, // Use the idToken as the auth token for the request method: 'POST', }) } catch (error) { log.verbose('oidc', `Failed token exchange request with body message: ${error?.body?.message || 'Unknown error'}`) return undefined } if (!response?.token) { log.verbose('oidc', 'Failed because token exchange was missing the token in the response body') return undefined } /* * The "opts" object is a clone of npm.flatOptions and is passed through the `publish` command, * eventually reaching `otplease`. To ensure the token is accessible during the publishing process, * it must be directly attached to the `opts` object. * Additionally, the token is required by the "live" configuration or getters within `config`. */ opts[authTokenKey] = response.token config.set(authTokenKey, response.token, 'user') log.verbose('oidc', `Successfully retrieved and set token`) try { const isDefaultProvenance = config.isDefault('provenance') if (isDefaultProvenance) { const [headerB64, payloadB64] = idToken.split('.') if (headerB64 && payloadB64) { const payloadJson = Buffer.from(payloadB64, 'base64').toString('utf8') const payload = JSON.parse(payloadJson) if ( (ciInfo.GITHUB_ACTIONS && payload.repository_visibility === 'public') || // only set provenance for gitlab if the repo is public and SIGSTORE_ID_TOKEN is available (ciInfo.GITLAB && payload.project_visibility === 'public' && process.env.SIGSTORE_ID_TOKEN) ) { const visibility = await libaccess.getVisibility(packageName, opts) if (visibility?.public) { log.verbose('oidc', `Enabling provenance`) opts.provenance = true config.set('provenance', true, 'user') } } } } } catch (error) { log.verbose('oidc', `Failed to set provenance with message: ${error?.message || 'Unknown error'}`) } } catch (error) { log.verbose('oidc', `Failure with message: ${error?.message || 'Unknown error'}`) } return undefined } module.exports = { oidc, }