Skip to content

Commit 3ded31e

Browse files
authored
Support publishing via OIDC authentication (#772)
1 parent 4f910a2 commit 3ded31e

File tree

5 files changed

+103
-1
lines changed

5 files changed

+103
-1
lines changed

source/npm/oidc.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import process from 'node:process';
2+
3+
const oidcProviders = [
4+
{
5+
id: 'github',
6+
name: 'GitHub Actions',
7+
// See https://github.com/npm/cli/blob/7da8fdd3625dd5541af57052c90fe1eabb41eb96/lib/utils/oidc.js#L49-L67
8+
// See https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#adding-permissions-settings
9+
validate: () =>
10+
process.env.GITHUB_ACTIONS
11+
&& process.env.ACTIONS_ID_TOKEN_REQUEST_URL
12+
&& process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
13+
},
14+
{
15+
id: 'gitlab',
16+
name: 'GitLab CI',
17+
// See https://github.com/npm/cli/blob/7da8fdd3625dd5541af57052c90fe1eabb41eb96/lib/utils/oidc.js#L37-L47
18+
validate: () => process.env.GITLAB_CI && process.env.NPM_ID_TOKEN,
19+
},
20+
];
21+
22+
export const getOidcProvider = () => {
23+
for (const provider of oidcProviders) {
24+
if (provider.validate()) {
25+
return provider.id;
26+
}
27+
}
28+
};

source/prerequisite-tasks.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Version from './version.js';
55
import * as util from './util.js';
66
import * as git from './git-util.js';
77
import * as npm from './npm/util.js';
8+
import {getOidcProvider} from './npm/oidc.js';
89

910
const prerequisiteTasks = (input, package_, options, packageManager) => {
1011
const isExternalRegistry = npm.isExternalRegistry(package_);
@@ -26,6 +27,11 @@ const prerequisiteTasks = (input, package_, options, packageManager) => {
2627
{
2728
title: 'Verify user is authenticated',
2829
enabled: () => process.env.NODE_ENV !== 'test' && !package_.private,
30+
skip() {
31+
if (getOidcProvider()) {
32+
return 'Environment support for OIDC authentication detected - Skipping whoami check';
33+
}
34+
},
2935
async task() {
3036
const externalRegistry = isExternalRegistry ? package_.publishConfig.registry : false;
3137
const username = await npm.username({externalRegistry});

test/_helpers/listr.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@ export const assertTaskDisabled = (t, taskTitle) => {
1818
export const assertTaskDoesntExist = (t, taskTitle) => {
1919
t.true(SilentRenderer.tasks.every(task => task.title !== taskTitle), `Task '${taskTitle}' exists!`);
2020
};
21+
22+
export const assertTaskSkipped = (t, taskTitle) => {
23+
const task = SilentRenderer.tasks.find(task => task.title === taskTitle);
24+
t.true(task.isSkipped(), `Task '${taskTitle}' was not skipped!`);
25+
};

test/npm/oidc.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import test from 'ava';
2+
import esmock from 'esmock';
3+
4+
test('detects GitHub Actions', async t => {
5+
const {getOidcProvider} = await esmock('../../source/npm/oidc.js', {
6+
'node:process': {
7+
env: {
8+
GITHUB_ACTIONS: 'true',
9+
ACTIONS_ID_TOKEN_REQUEST_URL: 'https://example.com',
10+
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
11+
},
12+
},
13+
});
14+
15+
t.is(getOidcProvider(), 'github');
16+
});
17+
18+
test('detects GitLab CI', async t => {
19+
const {getOidcProvider} = await esmock('../../source/npm/oidc.js', {
20+
'node:process': {
21+
env: {
22+
GITLAB_CI: 'true',
23+
NPM_ID_TOKEN: 'token',
24+
},
25+
},
26+
});
27+
28+
t.is(getOidcProvider(), 'gitlab');
29+
});
30+
31+
test('detects no OIDC', async t => {
32+
const {getOidcProvider} = await esmock('../../source/npm/oidc.js', {
33+
'node:process': {
34+
env: {},
35+
},
36+
});
37+
38+
t.is(getOidcProvider(), undefined);
39+
});

test/tasks/prerequisite-tasks.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import {npmConfig, yarnConfig} from '../../source/package-manager/configs.js';
44
import {npPackage} from '../../source/util.js';
55
import {SilentRenderer} from '../_helpers/listr-renderer.js';
66
import {_createFixture} from '../_helpers/stub-execa.js';
7-
import {run, assertTaskFailed, assertTaskDisabled} from '../_helpers/listr.js';
7+
import {
8+
run,
9+
assertTaskFailed,
10+
assertTaskDisabled,
11+
assertTaskSkipped,
12+
} from '../_helpers/listr.js';
813

914
/** @type {ReturnType<typeof _createFixture<import('../../source/prerequisite-tasks.js')>>} */
1015
const createFixture = _createFixture('../../source/prerequisite-tasks.js', import.meta.url);
@@ -237,3 +242,22 @@ test.serial('checks should pass', createFixture, [{
237242
}], async ({t, testedModule: prerequisiteTasks}) => {
238243
await t.notThrowsAsync(run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)));
239244
});
245+
246+
test.serial('should skip authentication check when OIDC is detected', createFixture, [{
247+
command: 'git rev-parse --quiet --verify refs/tags/v2.0.0',
248+
stdout: '',
249+
}], async ({t, testedModule: prerequisiteTasks}) => {
250+
process.env.NODE_ENV = 'P';
251+
process.env.GITHUB_ACTIONS = 'true';
252+
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'url';
253+
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'token';
254+
255+
await t.notThrowsAsync(run(prerequisiteTasks('2.0.0', {name: 'test', version: '1.0.0'}, {}, npmConfig)));
256+
257+
delete process.env.GITHUB_ACTIONS;
258+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
259+
delete process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
260+
process.env.NODE_ENV = 'test';
261+
262+
assertTaskSkipped(t, 'Verify user is authenticated');
263+
});

0 commit comments

Comments
 (0)