diff --git a/.yarn/versions/9ad4b6ac.yml b/.yarn/versions/9ad4b6ac.yml new file mode 100644 index 000000000000..5407f7d8e823 --- /dev/null +++ b/.yarn/versions/9ad4b6ac.yml @@ -0,0 +1,36 @@ +releases: + "@yarnpkg/cli": minor + "@yarnpkg/core": patch + "@yarnpkg/plugin-npm": minor + "@yarnpkg/plugin-npm-cli": minor + +declined: + - "@yarnpkg/plugin-catalog" + - "@yarnpkg/plugin-compat" + - "@yarnpkg/plugin-constraints" + - "@yarnpkg/plugin-dlx" + - "@yarnpkg/plugin-essentials" + - "@yarnpkg/plugin-exec" + - "@yarnpkg/plugin-file" + - "@yarnpkg/plugin-git" + - "@yarnpkg/plugin-github" + - "@yarnpkg/plugin-http" + - "@yarnpkg/plugin-init" + - "@yarnpkg/plugin-interactive-tools" + - "@yarnpkg/plugin-jsr" + - "@yarnpkg/plugin-link" + - "@yarnpkg/plugin-nm" + - "@yarnpkg/plugin-pack" + - "@yarnpkg/plugin-patch" + - "@yarnpkg/plugin-pnp" + - "@yarnpkg/plugin-pnpm" + - "@yarnpkg/plugin-stage" + - "@yarnpkg/plugin-typescript" + - "@yarnpkg/plugin-version" + - "@yarnpkg/plugin-workspace-tools" + - "@yarnpkg/builder" + - "@yarnpkg/doctor" + - "@yarnpkg/extensions" + - "@yarnpkg/nm" + - "@yarnpkg/pnpify" + - "@yarnpkg/sdks" diff --git a/packages/plugin-npm-cli/sources/commands/npm/publish.ts b/packages/plugin-npm-cli/sources/commands/npm/publish.ts index 6a8f8cb2d1ef..f4422311f23e 100644 --- a/packages/plugin-npm-cli/sources/commands/npm/publish.ts +++ b/packages/plugin-npm-cli/sources/commands/npm/publish.ts @@ -166,6 +166,7 @@ export default class NpmPublishCommand extends BaseCommand { ident, otp: this.otp, jsonResponse: true, + allowOidc: Boolean(process.env.CI && (process.env.GITHUB_ACTIONS || process.env.GITLAB)), }); } diff --git a/packages/plugin-npm/sources/npmHttpUtils.ts b/packages/plugin-npm/sources/npmHttpUtils.ts index b932ddaa569a..441b343cb4c4 100644 --- a/packages/plugin-npm/sources/npmHttpUtils.ts +++ b/packages/plugin-npm/sources/npmHttpUtils.ts @@ -26,6 +26,7 @@ type RegistryOptions = { export type Options = httpUtils.Options & RegistryOptions & { authType?: AuthType; + allowOidc?: boolean; otp?: string; }; @@ -316,13 +317,13 @@ function getMetadataFolder(configuration: Configuration) { return ppath.join(configuration.get(`globalFolder`), `metadata/npm`); } -export async function get(path: string, {configuration, headers, ident, authType, registry, ...rest}: Options) { +export async function get(path: string, {configuration, headers, ident, authType, allowOidc, registry, ...rest}: Options) { registry = normalizeRegistry(configuration, {ident, registry}); if (ident && ident.scope && typeof authType === `undefined`) authType = AuthType.BEST_EFFORT; - const auth = await getAuthenticationHeader(registry, {authType, configuration, ident}); + const auth = await getAuthenticationHeader(registry, {authType, allowOidc, configuration, ident}); if (auth) headers = {...headers, authorization: auth}; @@ -335,10 +336,10 @@ export async function get(path: string, {configuration, headers, ident, authType } } -export async function post(path: string, body: httpUtils.Body, {attemptedAs, configuration, headers, ident, authType = AuthType.ALWAYS_AUTH, registry, otp, ...rest}: Options & {attemptedAs?: string}) { +export async function post(path: string, body: httpUtils.Body, {attemptedAs, configuration, headers, ident, authType = AuthType.ALWAYS_AUTH, allowOidc, registry, otp, ...rest}: Options & {attemptedAs?: string}) { registry = normalizeRegistry(configuration, {ident, registry}); - const auth = await getAuthenticationHeader(registry, {authType, configuration, ident}); + const auth = await getAuthenticationHeader(registry, {authType, allowOidc, configuration, ident}); if (auth) headers = {...headers, authorization: auth}; if (otp) @@ -367,10 +368,10 @@ export async function post(path: string, body: httpUtils.Body, {attemptedAs, con } } -export async function put(path: string, body: httpUtils.Body, {attemptedAs, configuration, headers, ident, authType = AuthType.ALWAYS_AUTH, registry, otp, ...rest}: Options & {attemptedAs?: string}) { +export async function put(path: string, body: httpUtils.Body, {attemptedAs, configuration, headers, ident, authType = AuthType.ALWAYS_AUTH, allowOidc, registry, otp, ...rest}: Options & {attemptedAs?: string}) { registry = normalizeRegistry(configuration, {ident, registry}); - const auth = await getAuthenticationHeader(registry, {authType, configuration, ident}); + const auth = await getAuthenticationHeader(registry, {authType, allowOidc, configuration, ident}); if (auth) headers = {...headers, authorization: auth}; if (otp) @@ -399,10 +400,10 @@ export async function put(path: string, body: httpUtils.Body, {attemptedAs, conf } } -export async function del(path: string, {attemptedAs, configuration, headers, ident, authType = AuthType.ALWAYS_AUTH, registry, otp, ...rest}: Options & {attemptedAs?: string}) { +export async function del(path: string, {attemptedAs, configuration, headers, ident, authType = AuthType.ALWAYS_AUTH, allowOidc, registry, otp, ...rest}: Options & {attemptedAs?: string}) { registry = normalizeRegistry(configuration, {ident, registry}); - const auth = await getAuthenticationHeader(registry, {authType, configuration, ident}); + const auth = await getAuthenticationHeader(registry, {authType, allowOidc, configuration, ident}); if (auth) headers = {...headers, authorization: auth}; if (otp) @@ -441,7 +442,7 @@ function normalizeRegistry(configuration: Configuration, {ident, registry}: Part return npmConfigUtils.normalizeRegistry(registry); } -async function getAuthenticationHeader(registry: string, {authType = AuthType.CONFIGURATION, configuration, ident}: {authType?: AuthType, configuration: Configuration, ident: RegistryOptions[`ident`]}) { +async function getAuthenticationHeader(registry: string, {authType = AuthType.CONFIGURATION, allowOidc = false, configuration, ident}: {authType?: AuthType, allowOidc?: boolean, configuration: Configuration, ident: RegistryOptions[`ident`]}) { const effectiveConfiguration = npmConfigUtils.getAuthConfiguration(registry, {configuration, ident}); const mustAuthenticate = shouldAuthenticate(effectiveConfiguration, authType); @@ -465,6 +466,13 @@ async function getAuthenticationHeader(registry: string, {authType = AuthType.CO return `Basic ${npmAuthIdent}`; } + if (allowOidc && ident) { + const oidcToken = await getOidcToken(registry, {configuration, ident}); + if (oidcToken) { + return `Bearer ${oidcToken}`; + } + } + if (mustAuthenticate && authType !== AuthType.BEST_EFFORT) { throw new ReportError(MessageName.AUTHENTICATION_NOT_FOUND, `No authentication configured for request`); } else { @@ -575,3 +583,62 @@ function getOtpHeaders(otp: string) { [`npm-otp`]: otp, }; } + +/** + * This code is adapted from the npm project, under ISC License. + * + * Original source: + * https://github.com/npm/cli/blob/7d900c4656cfffc8cca93240c6cda4b441fbbfaa/lib/utils/oidc.js + */ +async function getOidcToken(registry: string, {configuration, ident}: {configuration: Configuration, ident: Ident}): Promise { + let idToken: string | null = null; + + if (process.env.GITLAB) { + idToken = process.env.NPM_ID_TOKEN || null; + } else if (process.env.GITHUB_ACTIONS) { + if (!(process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN)) + return null; + + // 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).host + // Yarn registry is an alias domain to the NPM registry. + .replace(`registry.yarnpkg.com`, `registry.npmjs.org`) + .replace(`yarn.npmjs.org`, `registry.npmjs.org`)}`; + + const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL); + url.searchParams.append(`audience`, audience); + + const response = await httpUtils.get(url.href, { + configuration, + jsonResponse: true, + headers: { + Authorization: `Bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`, + }, + }); + + idToken = response.value; + } + + if (!idToken) + return null; + + try { + const response = await httpUtils.post( + `${registry}/-/npm/v1/oidc/token/exchange/package/${ident.name.replace(/^@/, `%40`)}`, + null, + { + configuration, + jsonResponse: true, + headers: { + Authorization: `Bearer ${idToken}`, + }, + }, + ); + return response.token || null; + } catch { + // Best effort + } + + return null; +} diff --git a/packages/yarnpkg-core/sources/httpUtils.ts b/packages/yarnpkg-core/sources/httpUtils.ts index c83815ac3f2c..e6c03d22a424 100644 --- a/packages/yarnpkg-core/sources/httpUtils.ts +++ b/packages/yarnpkg-core/sources/httpUtils.ts @@ -210,19 +210,19 @@ export async function get(target: string, {configuration, jsonResponse, customEr } } -export async function put(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise { +export async function put(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise { const response = await prettyNetworkError(request(target, body, {...options, method: Method.PUT}), {customErrorMessage, configuration: options.configuration}); return response.body; } -export async function post(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise { +export async function post(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise { const response = await prettyNetworkError(request(target, body, {...options, method: Method.POST}), {customErrorMessage, configuration: options.configuration}); return response.body; } -export async function del(target: string, {customErrorMessage, ...options}: Options): Promise { +export async function del(target: string, {customErrorMessage, ...options}: Options): Promise { const response = await prettyNetworkError(request(target, null, {...options, method: Method.DELETE}), {customErrorMessage, configuration: options.configuration}); return response.body;