Skip to content
Merged
24 changes: 24 additions & 0 deletions .yarn/versions/9ad4b6ac.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
releases:
"@yarnpkg/cli": minor
"@yarnpkg/plugin-npm": minor
"@yarnpkg/plugin-npm-cli": minor

declined:
- "@yarnpkg/plugin-compat"
- "@yarnpkg/plugin-constraints"
- "@yarnpkg/plugin-dlx"
- "@yarnpkg/plugin-essentials"
- "@yarnpkg/plugin-init"
- "@yarnpkg/plugin-interactive-tools"
- "@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/core"
- "@yarnpkg/doctor"
1 change: 1 addition & 0 deletions packages/plugin-npm-cli/sources/commands/npm/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
});
}

Expand Down
78 changes: 69 additions & 9 deletions packages/plugin-npm/sources/npmHttpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type RegistryOptions = {

export type Options = httpUtils.Options & RegistryOptions & {
authType?: AuthType;
allowOidc?: boolean;
otp?: string;
};

Expand Down Expand Up @@ -314,13 +315,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};

Expand All @@ -333,10 +334,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)
Expand Down Expand Up @@ -365,10 +366,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)
Expand Down Expand Up @@ -397,10 +398,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)
Expand Down Expand Up @@ -439,7 +440,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);

Expand All @@ -463,6 +464,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 {
Expand Down Expand Up @@ -573,3 +581,55 @@ 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<string | null> {
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;

const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
url.searchParams.append(`audience`, `npm:${new URL(registry).host}`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. When using registry.yarnpkg.com, audience should be replaced with the original (registry.npmjs.com)


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;
}
6 changes: 3 additions & 3 deletions packages/yarnpkg-core/sources/httpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> {
export async function put(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise<any> {
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<Buffer> {
export async function post(target: string, body: Body, {customErrorMessage, ...options}: Options): Promise<any> {
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<Buffer> {
export async function del(target: string, {customErrorMessage, ...options}: Options): Promise<any> {
const response = await prettyNetworkError(request(target, null, {...options, method: Method.DELETE}), {customErrorMessage, configuration: options.configuration});

return response.body;
Expand Down
Loading