Skip to content

Commit e3319f1

Browse files
committed
feat(trusted-publishing): verify auth, considering OIDC vs tokens from various registriess
the trusted publishing verification is incomplete, but this change wires the various options together, at least for #958
1 parent c9f0da5 commit e3319f1

File tree

3 files changed

+121
-25
lines changed

3 files changed

+121
-25
lines changed

lib/verify-auth.js

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,54 @@ import AggregateError from "aggregate-error";
44
import getRegistry from "./get-registry.js";
55
import setNpmrcAuth from "./set-npmrc-auth.js";
66
import getError from "./get-error.js";
7+
import oidcContextEstablished from "./trusted-publishing/oidc-context.js";
78
import { OFFICIAL_REGISTRY } from "./definitions/constants.js";
89

910
function registryIsDefault(registry, DEFAULT_NPM_REGISTRY) {
1011
return normalizeUrl(registry) === normalizeUrl(DEFAULT_NPM_REGISTRY);
1112
}
1213

13-
function targetingDefaultRegistryButNoTokenProvided(registry, DEFAULT_NPM_REGISTRY, errors) {
14-
return registryIsDefault(registry, DEFAULT_NPM_REGISTRY) && errors.length === 1 && errors[0].code === "ENONPMTOKEN";
15-
}
16-
1714
export default async function (npmrc, pkg, context) {
1815
const {
1916
cwd,
2017
env: { DEFAULT_NPM_REGISTRY = OFFICIAL_REGISTRY, ...env },
2118
stdout,
2219
stderr,
23-
logger,
2420
} = context;
2521
const registry = getRegistry(pkg, context);
2622

27-
try {
28-
logger.log("setting npmrc auth for registry", registry);
29-
await setNpmrcAuth(npmrc, registry, context);
30-
} catch (aggregateError) {
31-
const { errors } = aggregateError;
32-
33-
if (targetingDefaultRegistryButNoTokenProvided(registry, DEFAULT_NPM_REGISTRY, errors)) {
34-
logger.log("NPM_TOKEN was not provided for the default npm registry.");
35-
} else {
36-
throw aggregateError;
37-
}
23+
if (oidcContextEstablished(registry)) {
24+
return;
3825
}
3926

27+
await setNpmrcAuth(npmrc, registry, context);
28+
4029
if (registryIsDefault(registry, DEFAULT_NPM_REGISTRY)) {
30+
try {
31+
const whoamiResult = execa("npm", ["whoami", "--userconfig", npmrc, "--registry", registry], {
32+
cwd,
33+
env,
34+
preferLocal: true,
35+
});
36+
whoamiResult.stdout.pipe(stdout, { end: false });
37+
whoamiResult.stderr.pipe(stderr, { end: false });
38+
await whoamiResult;
39+
} catch {
40+
throw new AggregateError([getError("EINVALIDNPMTOKEN", { registry })]);
41+
}
42+
} else {
4143
const publishDryRunResult = execa(
4244
"npm",
4345
["publish", "--dry-run", "--tag=semantic-release-auth-check", "--userconfig", npmrc, "--registry", registry],
4446
{ cwd, env, preferLocal: true, lines: true }
4547
);
4648

47-
publishDryRunResult.stdout.pipe(stdout, { end: false });
48-
publishDryRunResult.stderr.pipe(stderr, { end: false });
49+
// publishDryRunResult.stdout.pipe(stdout, { end: false });
50+
// publishDryRunResult.stderr.pipe(stderr, { end: false });
4951

5052
(await publishDryRunResult).stderr.forEach((line) => {
5153
if (line.includes("This command requires you to be logged in to ")) {
52-
throw new AggregateError([getError("EINVALIDNPMAUTH")]);
54+
throw new AggregateError([getError("EINVALIDNPMAUTH", { registry })]);
5355
}
5456
});
5557
}

test/integration.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ test('Skip npm token verification if "package.private" is true', async (t) => {
9191
);
9292
});
9393

94-
test("Throws error if NPM token is invalid", async (t) => {
94+
test("Throws error if NPM token is invalid when targeting the default registry", async (t) => {
9595
const cwd = temporaryDirectory();
9696
const env = { NPM_TOKEN: "wrong_token", DEFAULT_NPM_REGISTRY: npmRegistry.url };
9797
const pkg = { name: "published", version: "1.0.0", publishConfig: { registry: npmRegistry.url } };
@@ -111,7 +111,7 @@ test("Throws error if NPM token is invalid", async (t) => {
111111
t.is(error.message, "Invalid npm authentication.");
112112
});
113113

114-
test("Throws error if NPM token is not provided", async (t) => {
114+
test("Throws error if NPM token is not provided when targeting the default registry", async (t) => {
115115
const cwd = temporaryDirectory();
116116
const env = { DEFAULT_NPM_REGISTRY: npmRegistry.url };
117117
const pkg = { name: "published", version: "1.0.0", publishConfig: { registry: npmRegistry.url } };

test/verify-auth.test.js

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import test from "ava";
22
import * as td from "testdouble";
3+
import AggregateError from "aggregate-error";
34
import { OFFICIAL_REGISTRY } from "../lib/definitions/constants.js";
45

5-
let execa, verifyAuth, getRegistry, setNpmrcAuth;
6+
let execa, verifyAuth, getRegistry, setNpmrcAuth, oidcContextEstablished;
67
const DEFAULT_NPM_REGISTRY = OFFICIAL_REGISTRY;
78
const npmrc = "npmrc contents";
89
const pkg = {};
@@ -15,6 +16,8 @@ test.beforeEach(async (t) => {
1516
({ execa } = await td.replaceEsm("execa"));
1617
({ default: getRegistry } = await td.replaceEsm("../lib/get-registry.js"));
1718
({ default: setNpmrcAuth } = await td.replaceEsm("../lib/set-npmrc-auth.js"));
19+
({ default: oidcContextEstablished } = await td.replaceEsm("../lib/trusted-publishing/oidc-context.js"));
20+
td.when(oidcContextEstablished()).thenReturn(false);
1821

1922
({ default: verifyAuth } = await import("../lib/verify-auth.js"));
2023
});
@@ -23,10 +26,21 @@ test.afterEach.always((t) => {
2326
td.reset();
2427
});
2528

29+
test.serial(
30+
"that the auth context for the official registry is considered valid when trusted publishing is established",
31+
async (t) => {
32+
td.when(getRegistry(pkg, context)).thenReturn(DEFAULT_NPM_REGISTRY);
33+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY)).thenReturn(true);
34+
35+
await t.notThrowsAsync(verifyAuth(npmrc, pkg, context));
36+
}
37+
);
38+
2639
test.serial(
2740
"that the provided token is verified with `npm whoami` when trusted publishing is not established for the official registry",
2841
async (t) => {
2942
td.when(getRegistry(pkg, context)).thenReturn(DEFAULT_NPM_REGISTRY);
43+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY)).thenReturn(false);
3044
td.when(
3145
execa("npm", ["whoami", "--userconfig", npmrc, "--registry", DEFAULT_NPM_REGISTRY], {
3246
cwd,
@@ -46,6 +60,7 @@ test.serial(
4660
"that the auth context for the official registry is considered invalid when no token is provided and trusted publishing is not established",
4761
async (t) => {
4862
td.when(getRegistry(pkg, context)).thenReturn(DEFAULT_NPM_REGISTRY);
63+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY)).thenReturn(false);
4964
td.when(
5065
execa("npm", ["whoami", "--userconfig", npmrc, "--registry", DEFAULT_NPM_REGISTRY], {
5166
cwd,
@@ -64,10 +79,89 @@ test.serial(
6479
}
6580
);
6681

82+
test.serial(
83+
"that a publish dry run is performed to validate token presence when publishing to a custom registry",
84+
async (t) => {
85+
const otherRegistry = "https://other.registry.org";
86+
td.when(getRegistry(pkg, context)).thenReturn(otherRegistry);
87+
td.when(oidcContextEstablished(otherRegistry)).thenReturn(false);
88+
td.when(
89+
execa(
90+
"npm",
91+
[
92+
"publish",
93+
"--dry-run",
94+
"--tag=semantic-release-auth-check",
95+
"--userconfig",
96+
npmrc,
97+
"--registry",
98+
otherRegistry,
99+
],
100+
{
101+
cwd,
102+
env: otherEnvVars,
103+
preferLocal: true,
104+
lines: true,
105+
}
106+
)
107+
).thenResolve({
108+
stderr: ["foo", "bar", "baz"],
109+
});
110+
111+
await t.notThrowsAsync(verifyAuth(npmrc, pkg, context));
112+
}
113+
);
114+
67115
// since alternative registries are not consistent in implementing `npm whoami`,
68116
// we do not attempt to verify the provided token when publishing to them
69-
test.serial("that `npm whoami` is not invoked when publishing to a custom registry", async (t) => {
70-
td.when(getRegistry(pkg, context)).thenReturn("https://other.registry.org");
117+
test.serial(
118+
"that the token is considered invalid when the publish dry run fails when publishing to a custom registry",
119+
async (t) => {
120+
const otherRegistry = "https://other.registry.org";
121+
td.when(getRegistry(pkg, context)).thenReturn(otherRegistry);
122+
td.when(oidcContextEstablished(otherRegistry)).thenReturn(false);
123+
td.when(
124+
execa(
125+
"npm",
126+
[
127+
"publish",
128+
"--dry-run",
129+
"--tag=semantic-release-auth-check",
130+
"--userconfig",
131+
npmrc,
132+
"--registry",
133+
otherRegistry,
134+
],
135+
{
136+
cwd,
137+
env: otherEnvVars,
138+
preferLocal: true,
139+
lines: true,
140+
}
141+
)
142+
).thenResolve({
143+
stderr: ["foo", "bar", "baz", `This command requires you to be logged in to ${otherRegistry}`, "qux"],
144+
});
145+
146+
const {
147+
errors: [error],
148+
} = await t.throwsAsync(verifyAuth(npmrc, pkg, context));
149+
150+
t.is(error.name, "SemanticReleaseError");
151+
t.is(error.code, "EINVALIDNPMAUTH");
152+
t.is(error.message, "Invalid npm authentication.");
153+
}
154+
);
155+
156+
test.serial("that errors from setting up auth bubble through this function", async (t) => {
157+
const registry = DEFAULT_NPM_REGISTRY;
158+
const thrownError = new Error();
159+
td.when(getRegistry(pkg, context)).thenReturn(registry);
160+
td.when(setNpmrcAuth(npmrc, registry, context)).thenThrow(new AggregateError([thrownError]));
161+
162+
const {
163+
errors: [error],
164+
} = await t.throwsAsync(verifyAuth(npmrc, pkg, context));
71165

72-
await t.notThrowsAsync(verifyAuth(npmrc, pkg, context));
166+
t.is(error, thrownError);
73167
});

0 commit comments

Comments
 (0)