Skip to content

Commit c80ecb0

Browse files
committed
feat(trusted-publishing): make request to verify if OIDC token exchange can succeed
this is the correct call, but details are still incomplete since the bearer token for the request needs to be the OIDC token from the CI IdP for #958
1 parent e75c620 commit c80ecb0

File tree

7 files changed

+75
-16
lines changed

7 files changed

+75
-16
lines changed

lib/set-npmrc-auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { OFFICIAL_REGISTRY } from "./definitions/constants.js";
99

1010
export default async function (npmrc, registry, { cwd, env: { NPM_TOKEN, NPM_CONFIG_USERCONFIG }, logger }) {
1111
logger.log("Verify authentication for registry %s", registry);
12-
const { configs, ...rcConfig } = rc(
12+
const { configs, config, ...rcConfig } = rc(
1313
"npm",
1414
{ registry: OFFICIAL_REGISTRY },
1515
{ config: NPM_CONFIG_USERCONFIG || path.resolve(cwd, ".npmrc") }
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OFFICIAL_REGISTRY } from "../definitions/constants.js";
22
import trustedCiProvider from "./supported-ci-provider.js";
3+
import tokenExchange from "./token-exchange.js";
34

4-
export default function oidcContextEstablished(registry) {
5-
return OFFICIAL_REGISTRY === registry && trustedCiProvider();
5+
export default async function oidcContextEstablished(registry, pkg) {
6+
return OFFICIAL_REGISTRY === registry && trustedCiProvider() && !!(await tokenExchange(pkg));
67
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { OFFICIAL_REGISTRY } from "../definitions/constants.js";
2+
3+
export default async function tokenExchange(pkg) {
4+
const response = await fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${pkg.name}`, { method: 'POST' });
5+
6+
if (response.ok) {
7+
return (await response.json()).token;
8+
}
9+
10+
return undefined;
11+
}

lib/verify-auth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export default async function (npmrc, pkg, context) {
6262
} = context;
6363
const registry = getRegistry(pkg, context);
6464

65-
if (oidcContextEstablished(registry)) {
65+
if (oidcContextEstablished(registry, pkg)) {
6666
return;
6767
}
6868

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

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ import * as td from "testdouble";
33

44
import { OFFICIAL_REGISTRY } from "../../lib/definitions/constants.js";
55

6-
let oidcContextEstablished, trustedCiProvider;
6+
let oidcContextEstablished, trustedCiProvider, tokenExchange;
7+
const pkg = {};
78

89
test.beforeEach(async (t) => {
10+
await td.replace(globalThis, "fetch");
911
({ default: trustedCiProvider } = await td.replaceEsm("../../lib/trusted-publishing/supported-ci-provider.js"));
12+
({ default: tokenExchange } = await td.replaceEsm("../../lib/trusted-publishing/token-exchange.js"));
13+
td.when(tokenExchange(pkg)).thenResolve(undefined);
1014

1115
({ default: oidcContextEstablished } = await import("../../lib/trusted-publishing/oidc-context.js"));
1216
});
@@ -15,18 +19,28 @@ test.afterEach.always((t) => {
1519
td.reset();
1620
});
1721

18-
test("that `true` is returned when a trusted-publishing context has been established with the official registry", async (t) => {
22+
test.serial("that `true` is returned when a trusted-publishing context has been established with the official registry", async (t) => {
1923
td.when(trustedCiProvider()).thenResolve(true);
24+
td.when(fetch("https://matt.travi.org")).thenResolve(new Response(null, { status: 401 }));
25+
td.when(tokenExchange(pkg)).thenResolve('token-value');
2026

21-
t.true(await oidcContextEstablished(OFFICIAL_REGISTRY));
27+
t.true(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg));
2228
});
2329

24-
test("that `false` is returned when the official registry is targeted, but outside the context of a supported CI provider", async (t) => {
30+
test.serial("that `false` is returned when the official registry is targeted, but outside the context of a supported CI provider", async (t) => {
2531
td.when(trustedCiProvider()).thenResolve(false);
2632

27-
t.false(await oidcContextEstablished(OFFICIAL_REGISTRY));
33+
t.false(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg));
2834
});
2935

30-
test("that `false` is returned when a custom registry is targeted", async (t) => {
31-
t.false(await oidcContextEstablished("https://custom.registry.org/"));
36+
test.serial("that `false` is returned when OIDC token exchange fails in a supported CI provider", async (t) => {
37+
td.when(trustedCiProvider()).thenResolve(true);
38+
td.when(fetch("https://matt.travi.org")).thenResolve(new Response(null, { status: 401 }));
39+
td.when(tokenExchange(pkg)).thenResolve(undefined);
40+
41+
t.false(await oidcContextEstablished(OFFICIAL_REGISTRY, pkg));
42+
});
43+
44+
test.serial("that `false` is returned when a custom registry is targeted", async (t) => {
45+
t.false(await oidcContextEstablished("https://custom.registry.org/", pkg));
3246
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import test from "ava";
2+
import * as td from "testdouble";
3+
4+
import tokenExchange from '../../lib/trusted-publishing/token-exchange.js';
5+
import { OFFICIAL_REGISTRY } from "../../lib/definitions/constants.js";
6+
7+
// https://api-docs.npmjs.com/#tag/registry.npmjs.org/operation/exchangeOidcToken
8+
9+
const packageName = "some-package";
10+
const pkg = { name: packageName };
11+
12+
test.beforeEach(async (t) => {
13+
await td.replace(globalThis, "fetch");
14+
});
15+
16+
test.afterEach.always((t) => {
17+
td.reset();
18+
});
19+
20+
test.serial("that an access token is returned when token exchange succeeds", async (t) => {
21+
const token = "token-value";
22+
td.when(fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${packageName}`, {method: 'POST'}))
23+
.thenResolve(new Response(JSON.stringify({token}), {status: 201, headers: {'Content-Type': 'application/json'}}));
24+
25+
t.is(await tokenExchange(pkg), token);
26+
});
27+
28+
test.serial("that `undefined` is returned when token exchange fails", async (t) => {
29+
td.when(fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${packageName}`, {method: 'POST'}))
30+
.thenResolve(new Response(JSON.stringify({message: 'foo'}), {status: 401, headers: {'Content-Type': 'application/json'}}));
31+
32+
t.is(await tokenExchange(pkg), undefined);
33+
});

test/verify-auth.test.js

Lines changed: 5 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)).thenReturn(true);
33+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg)).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)).thenReturn(false);
43+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg)).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)).thenReturn(false);
63+
td.when(oidcContextEstablished(DEFAULT_NPM_REGISTRY, pkg)).thenReturn(false);
6464
td.when(
6565
execa("npm", ["whoami", "--userconfig", npmrc, "--registry", DEFAULT_NPM_REGISTRY], {
6666
cwd,
@@ -84,7 +84,7 @@ test.serial(
8484
async (t) => {
8585
const otherRegistry = "https://other.registry.org";
8686
td.when(getRegistry(pkg, context)).thenReturn(otherRegistry);
87-
td.when(oidcContextEstablished(otherRegistry)).thenReturn(false);
87+
td.when(oidcContextEstablished(otherRegistry, pkg)).thenReturn(false);
8888
td.when(
8989
execa(
9090
"npm",
@@ -119,7 +119,7 @@ test.serial(
119119
async (t) => {
120120
const otherRegistry = "https://other.registry.org";
121121
td.when(getRegistry(pkg, context)).thenReturn(otherRegistry);
122-
td.when(oidcContextEstablished(otherRegistry)).thenReturn(false);
122+
td.when(oidcContextEstablished(otherRegistry, pkg)).thenReturn(false);
123123
td.when(
124124
execa(
125125
"npm",

0 commit comments

Comments
 (0)