Skip to content

Commit a4a5add

Browse files
committed
feat(trusted-publishing): log progression of token-exchange steps
for #958
1 parent 67ee603 commit a4a5add

File tree

6 files changed

+45
-30
lines changed

6 files changed

+45
-30
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { OFFICIAL_REGISTRY } from "../definitions/constants.js";
22
import exchangeToken from "./token-exchange.js";
33

4-
export default async function oidcContextEstablished(registry, pkg) {
5-
return OFFICIAL_REGISTRY === registry && !!(await exchangeToken(pkg));
4+
export default async function oidcContextEstablished(registry, pkg, context) {
5+
return OFFICIAL_REGISTRY === registry && !!(await exchangeToken(pkg, context));
66
}

lib/trusted-publishing/token-exchange.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,53 +7,65 @@ import {
77
GITLAB_PIPELINES_PROVIDER_NAME,
88
} from "../definitions/constants.js";
99

10-
async function exchangeIdToken(idToken, packageName) {
10+
async function exchangeIdToken(idToken, packageName, logger) {
1111
const response = await fetch(
1212
`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`,
1313
{
1414
method: "POST",
1515
headers: { Authorization: `Bearer ${idToken}` },
1616
}
1717
);
18+
const responseBody = await response.json();
1819

1920
if (response.ok) {
20-
return (await response.json()).token;
21+
logger.log("OIDC token exchange with the npm registry succeeded");
22+
23+
return responseBody.token;
2124
}
2225

26+
logger.log(`OIDC token exchange with the npm registry failed: ${response.status} ${responseBody.message}`);
27+
2328
return undefined;
2429
}
2530

26-
async function exchangeGithubActionsToken(packageName) {
31+
async function exchangeGithubActionsToken(packageName, logger) {
2732
let idToken;
2833

34+
logger.log("Verifying OIDC context for publishing from GitHub Actions");
35+
2936
try {
3037
idToken = await getIDToken("npm:registry.npmjs.org");
3138
} catch (e) {
39+
logger.log(`Retrieval of GitHub Actions OIDC token failed: ${e.message}`);
40+
logger.log("Have you granted the `id-token: write` permission to this workflow?");
41+
3242
return undefined;
3343
}
3444

35-
return exchangeIdToken(idToken, packageName);
45+
return exchangeIdToken(idToken, packageName, logger);
3646
}
3747

38-
async function exchangeGitlabPipelinesToken(packageName) {
48+
async function exchangeGitlabPipelinesToken(packageName, logger) {
3949
const idToken = process.env.NPM_ID_TOKEN;
4050

51+
logger.log("Verifying OIDC context for publishing from GitLab Pipelines");
52+
4153
if (!idToken) {
4254
return undefined;
4355
}
4456

45-
return await exchangeIdToken(idToken, packageName);
57+
return exchangeIdToken(idToken, packageName, logger);
4658
}
4759

48-
export default async function exchangeToken(pkg) {
60+
export default function exchangeToken(pkg, {logger}) {
4961
const { name: ciProviderName } = envCi();
5062

5163
if (GITHUB_ACTIONS_PROVIDER_NAME === ciProviderName) {
52-
return await exchangeGithubActionsToken(pkg.name);
64+
return exchangeGithubActionsToken(pkg.name, logger);
5365
}
5466

5567
if (GITLAB_PIPELINES_PROVIDER_NAME === ciProviderName) {
56-
return await exchangeGitlabPipelinesToken(pkg.name);
68+
return exchangeGitlabPipelinesToken(pkg.name, logger);
5769
}
5870

5971
return undefined;

lib/verify-auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ async function verifyTokenAuth(registry, npmrc, context) {
7474
export default async function (npmrc, pkg, context) {
7575
const registry = getRegistry(pkg, context);
7676

77-
if (oidcContextEstablished(registry, pkg)) {
77+
if (oidcContextEstablished(registry, pkg, context)) {
7878
return;
7979
}
8080

test/trusted-publishing/oidc-context.test.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { OFFICIAL_REGISTRY } from "../../lib/definitions/constants.js";
55

66
let oidcContextEstablished, trustedCiProvider, exchangeToken;
77
const pkg = {};
8+
const context = {};
89

910
test.beforeEach(async (t) => {
1011
await td.replace(globalThis, "fetch");
1112
({ default: exchangeToken } = await td.replaceEsm("../../lib/trusted-publishing/token-exchange.js"));
12-
td.when(exchangeToken(pkg)).thenResolve(undefined);
13+
td.when(exchangeToken(pkg, context)).thenResolve(undefined);
1314

1415
({ default: oidcContextEstablished } = await import("../../lib/trusted-publishing/oidc-context.js"));
1516
});
@@ -22,19 +23,19 @@ test.serial(
2223
"that `true` is returned when a trusted-publishing context has been established with the official registry",
2324
async (t) => {
2425
td.when(fetch("https://matt.travi.org")).thenResolve(new Response(null, { status: 401 }));
25-
td.when(exchangeToken(pkg)).thenResolve("token-value");
26+
td.when(exchangeToken(pkg, context)).thenResolve("token-value");
2627

27-
t.true(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg));
28+
t.true(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg, context));
2829
}
2930
);
3031

3132
test.serial("that `false` is returned when OIDC token exchange fails in a supported CI provider", async (t) => {
3233
td.when(fetch("https://matt.travi.org")).thenResolve(new Response(null, { status: 401 }));
33-
td.when(exchangeToken(pkg)).thenResolve(undefined);
34+
td.when(exchangeToken(pkg, context)).thenResolve(undefined);
3435

35-
t.false(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg));
36+
t.false(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg, context));
3637
});
3738

3839
test.serial("that `false` is returned when a custom registry is targeted", async (t) => {
39-
t.false(await oidcContextEstablished("https://custom.registry.org/", pkg));
40+
t.false(await oidcContextEstablished("https://custom.registry.org/", pkg, context));
4041
});

test/trusted-publishing/token-exchange.test.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const packageName = "@scope/some-package";
1414
const pkg = { name: packageName };
1515
const idToken = "id-token-value";
1616
const token = "token-value";
17+
const logger = { log: () => undefined };
1718

1819
test.beforeEach(async (t) => {
1920
await td.replace(globalThis, "fetch");
@@ -41,7 +42,7 @@ test.serial("that an access token is returned when token exchange succeeds on Gi
4142
new Response(JSON.stringify({ token }), { status: 201, headers: { "Content-Type": "application/json" } })
4243
);
4344

44-
t.is(await exchangeToken(pkg), token);
45+
t.is(await exchangeToken(pkg, {logger}), token);
4546
});
4647

4748
test.serial("that `undefined` is returned when ID token retrieval fails on GitHub Actions", async (t) => {
@@ -50,7 +51,7 @@ test.serial("that `undefined` is returned when ID token retrieval fails on GitHu
5051
new Error("Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable")
5152
);
5253

53-
t.is(await exchangeToken(pkg), undefined);
54+
t.is(await exchangeToken(pkg, {logger}), undefined);
5455
});
5556

5657
test.serial("that `undefined` is returned when token exchange fails on GitHub Actions", async (t) => {
@@ -65,7 +66,7 @@ test.serial("that `undefined` is returned when token exchange fails on GitHub Ac
6566
new Response(JSON.stringify({ message: "foo" }), { status: 401, headers: { "Content-Type": "application/json" } })
6667
);
6768

68-
t.is(await exchangeToken(pkg), undefined);
69+
t.is(await exchangeToken(pkg, {logger}), undefined);
6970
});
7071

7172
test.serial("that an access token is returned when token exchange succeeds on GitLab Pipelines", async (t) => {
@@ -80,13 +81,13 @@ test.serial("that an access token is returned when token exchange succeeds on Gi
8081
new Response(JSON.stringify({ token }), { status: 201, headers: { "Content-Type": "application/json" } })
8182
);
8283

83-
t.is(await exchangeToken(pkg), token);
84+
t.is(await exchangeToken(pkg, {logger}), token);
8485
});
8586

8687
test.serial("that `undefined` is returned when ID token is not available on GitLab Pipelines", async (t) => {
8788
td.when(envCi()).thenReturn({ name: GITLAB_PIPELINES_PROVIDER_NAME });
8889

89-
t.is(await exchangeToken(pkg), undefined);
90+
t.is(await exchangeToken(pkg, {logger}), undefined);
9091
});
9192

9293
test.serial("that `undefined` is returned when token exchange fails on GitLab Pipelines", async (t) => {
@@ -101,11 +102,11 @@ test.serial("that `undefined` is returned when token exchange fails on GitLab Pi
101102
new Response(JSON.stringify({ message: "foo" }), { status: 401, headers: { "Content-Type": "application/json" } })
102103
);
103104

104-
t.is(await exchangeToken(pkg), undefined);
105+
t.is(await exchangeToken(pkg, {logger}), undefined);
105106
});
106107

107108
test.serial("that `undefined` is returned when no supported CI provider is detected", async (t) => {
108109
td.when(envCi()).thenReturn({ name: "Other Service" });
109110

110-
t.is(await exchangeToken(pkg), undefined);
111+
t.is(await exchangeToken(pkg, {logger}), undefined);
111112
});

test/verify-auth.test.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ test.serial(
3030
"that the auth context for the official registry is considered valid when trusted publishing is established",
3131
async (t) => {
3232
td.when(getRegistry(pkg, context)).thenReturn(DEFAULT_NPM_REGISTRY);
33-
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg)).thenReturn(true);
33+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg, context)).thenReturn(true);
3434

3535
await t.notThrowsAsync(verifyAuth(npmrc, pkg, context));
3636
}
@@ -40,7 +40,7 @@ test.serial(
4040
"that the provided token is verified with `npm whoami` when trusted publishing is not established for the official registry",
4141
async (t) => {
4242
td.when(getRegistry(pkg, context)).thenReturn(DEFAULT_NPM_REGISTRY);
43-
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg)).thenReturn(false);
43+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg, context)).thenReturn(false);
4444
td.when(
4545
execa("npm", ["whoami", "--userconfig", npmrc, "--registry", DEFAULT_NPM_REGISTRY], {
4646
cwd,
@@ -60,7 +60,7 @@ test.serial(
6060
"that the auth context for the official registry is considered invalid when no token is provided and trusted publishing is not established",
6161
async (t) => {
6262
td.when(getRegistry(pkg, context)).thenReturn(DEFAULT_NPM_REGISTRY);
63-
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg)).thenReturn(false);
63+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg, context)).thenReturn(false);
6464
td.when(
6565
execa("npm", ["whoami", "--userconfig", npmrc, "--registry", DEFAULT_NPM_REGISTRY], {
6666
cwd,
@@ -89,7 +89,7 @@ test.serial(
8989
execaResult.stderr = { pipe: () => undefined };
9090
execaResult.stdout = { pipe: () => undefined };
9191
td.when(getRegistry(pkg, context)).thenReturn(otherRegistry);
92-
td.when(oidcContextEstablished(otherRegistry, pkg)).thenReturn(false);
92+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg, context)).thenReturn(false);
9393
td.when(
9494
execa(
9595
"npm",
@@ -127,7 +127,7 @@ test.serial(
127127
execaResult.stderr = { pipe: () => undefined };
128128
execaResult.stdout = { pipe: () => undefined };
129129
td.when(getRegistry(pkg, context)).thenReturn(otherRegistry);
130-
td.when(oidcContextEstablished(otherRegistry, pkg)).thenReturn(false);
130+
td.when(oidcContextEstablished(otherRegistry, pkg, context)).thenReturn(false);
131131
td.when(
132132
execa(
133133
"npm",
@@ -163,6 +163,7 @@ test.serial("that errors from setting up auth bubble through this function", asy
163163
const registry = DEFAULT_NPM_REGISTRY;
164164
const thrownError = new Error();
165165
td.when(getRegistry(pkg, context)).thenReturn(registry);
166+
td.when(oidcContextEstablished(registry, pkg, context)).thenReturn(false);
166167
td.when(setNpmrcAuth(npmrc, registry, context)).thenThrow(new AggregateError([thrownError]));
167168

168169
const {

0 commit comments

Comments
 (0)