Skip to content

Commit 6d1c3cf

Browse files
committed
feat(trusted-publishing): pass id-token as bearer header for gitlab pipelines
for #958
1 parent b673257 commit 6d1c3cf

File tree

3 files changed

+103
-20
lines changed

3 files changed

+103
-20
lines changed

lib/definitions/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
export const OFFICIAL_REGISTRY = "https://registry.npmjs.org/";
2+
3+
export const GITHUB_ACTIONS_PROVIDER_NAME = "GitHub Actions";
4+
export const GITLAB_PIPELINES_PROVIDER_NAME = "GitLab CI/CD";
Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import { getIDToken } from "@actions/core";
2+
import envCi from "env-ci";
23

3-
import { OFFICIAL_REGISTRY } from "../definitions/constants.js";
4-
5-
const GITHUB_ACTIONS_PROVIDER_NAME = "GitHub Actions";
6-
const GITLAB_PIPELINES_PROVIDER_NAME = "GitLab CI/CD";
7-
8-
async function exchangeGithubActionsToken(packageName) {
9-
let idToken;
10-
11-
try {
12-
idToken = await getIDToken("npm:registry.npmjs.org");
13-
} catch (e) {
14-
return undefined;
15-
}
4+
import {
5+
OFFICIAL_REGISTRY,
6+
GITHUB_ACTIONS_PROVIDER_NAME,
7+
GITLAB_PIPELINES_PROVIDER_NAME,
8+
} from "../definitions/constants.js";
169

10+
async function exchangeIdToken(idToken, packageName) {
1711
const response = await fetch(
1812
`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`,
1913
{
@@ -29,6 +23,38 @@ async function exchangeGithubActionsToken(packageName) {
2923
return undefined;
3024
}
3125

26+
async function exchangeGithubActionsToken(packageName) {
27+
let idToken;
28+
29+
try {
30+
idToken = await getIDToken("npm:registry.npmjs.org");
31+
} catch (e) {
32+
return undefined;
33+
}
34+
35+
return exchangeIdToken(idToken, packageName);
36+
}
37+
38+
async function exchangeGitlabPipelinesToken(packageName) {
39+
const idToken = process.env.NPM_ID_TOKEN;
40+
41+
if (!idToken) {
42+
return undefined;
43+
}
44+
45+
return await exchangeIdToken(idToken, packageName);
46+
}
47+
3248
export default async function exchangeToken(pkg) {
33-
return await exchangeGithubActionsToken(pkg.name);
49+
const { name: ciProviderName } = envCi();
50+
51+
if (GITHUB_ACTIONS_PROVIDER_NAME === ciProviderName) {
52+
return await exchangeGithubActionsToken(pkg.name);
53+
}
54+
55+
if (GITLAB_PIPELINES_PROVIDER_NAME === ciProviderName) {
56+
return await exchangeGitlabPipelinesToken(pkg.name);
57+
}
58+
59+
return undefined;
3460
}

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

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import test from "ava";
22
import * as td from "testdouble";
33

4-
import { OFFICIAL_REGISTRY } from "../../lib/definitions/constants.js";
4+
import {
5+
OFFICIAL_REGISTRY,
6+
GITHUB_ACTIONS_PROVIDER_NAME,
7+
GITLAB_PIPELINES_PROVIDER_NAME,
8+
} from "../../lib/definitions/constants.js";
59

610
// https://api-docs.npmjs.com/#tag/registry.npmjs.org/operation/exchangeOidcToken
711

8-
let exchangeToken, getIDToken;
12+
let exchangeToken, getIDToken, envCi;
913
const packageName = "@scope/some-package";
1014
const pkg = { name: packageName };
1115
const idToken = "id-token-value";
@@ -14,15 +18,19 @@ const token = "token-value";
1418
test.beforeEach(async (t) => {
1519
await td.replace(globalThis, "fetch");
1620
({ getIDToken } = await td.replaceEsm("@actions/core"));
21+
({ default: envCi } = await td.replaceEsm("env-ci"));
1722

1823
({ default: exchangeToken } = await import("../../lib/trusted-publishing/token-exchange.js"));
1924
});
2025

2126
test.afterEach.always((t) => {
2227
td.reset();
28+
29+
delete process.env.NPM_ID_TOKEN;
2330
});
2431

25-
test.serial("that an access token is returned when token exchange succeeds", async (t) => {
32+
test.serial("that an access token is returned when token exchange succeeds on GitHub Actions", async (t) => {
33+
td.when(envCi()).thenReturn({ name: GITHUB_ACTIONS_PROVIDER_NAME });
2634
td.when(getIDToken("npm:registry.npmjs.org")).thenResolve(idToken);
2735
td.when(
2836
fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`, {
@@ -36,13 +44,17 @@ test.serial("that an access token is returned when token exchange succeeds", asy
3644
t.is(await exchangeToken(pkg), token);
3745
});
3846

39-
test.serial("that `undefined` is returned when ID token retrieval fails", async (t) => {
40-
td.when(getIDToken("npm:registry.npmjs.org")).thenThrow(new Error("Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable"));
47+
test.serial("that `undefined` is returned when ID token retrieval fails on GitHub Actions", async (t) => {
48+
td.when(envCi()).thenReturn({ name: GITHUB_ACTIONS_PROVIDER_NAME });
49+
td.when(getIDToken("npm:registry.npmjs.org")).thenThrow(
50+
new Error("Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable")
51+
);
4152

4253
t.is(await exchangeToken(pkg), undefined);
4354
});
4455

45-
test.serial("that `undefined` is returned when token exchange fails", async (t) => {
56+
test.serial("that `undefined` is returned when token exchange fails on GitHub Actions", async (t) => {
57+
td.when(envCi()).thenReturn({ name: GITHUB_ACTIONS_PROVIDER_NAME });
4658
td.when(getIDToken("npm:registry.npmjs.org")).thenResolve(idToken);
4759
td.when(
4860
fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`, {
@@ -55,3 +67,45 @@ test.serial("that `undefined` is returned when token exchange fails", async (t)
5567

5668
t.is(await exchangeToken(pkg), undefined);
5769
});
70+
71+
test.serial("that an access token is returned when token exchange succeeds on GitLab Pipelines", async (t) => {
72+
process.env.NPM_ID_TOKEN = idToken;
73+
td.when(envCi()).thenReturn({ name: GITLAB_PIPELINES_PROVIDER_NAME });
74+
td.when(
75+
fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`, {
76+
method: "POST",
77+
headers: { Authorization: `Bearer ${idToken}` },
78+
})
79+
).thenResolve(
80+
new Response(JSON.stringify({ token }), { status: 201, headers: { "Content-Type": "application/json" } })
81+
);
82+
83+
t.is(await exchangeToken(pkg), token);
84+
});
85+
86+
test.serial("that `undefined` is returned when ID token is not available on GitLab Pipelines", async (t) => {
87+
td.when(envCi()).thenReturn({ name: GITLAB_PIPELINES_PROVIDER_NAME });
88+
89+
t.is(await exchangeToken(pkg), undefined);
90+
});
91+
92+
test.serial("that `undefined` is returned when token exchange fails on GitLab Pipelines", async (t) => {
93+
process.env.NPM_ID_TOKEN = idToken;
94+
td.when(envCi()).thenReturn({ name: GITLAB_PIPELINES_PROVIDER_NAME });
95+
td.when(
96+
fetch(`${OFFICIAL_REGISTRY}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`, {
97+
method: "POST",
98+
headers: { Authorization: `Bearer ${idToken}` },
99+
})
100+
).thenResolve(
101+
new Response(JSON.stringify({ message: "foo" }), { status: 401, headers: { "Content-Type": "application/json" } })
102+
);
103+
104+
t.is(await exchangeToken(pkg), undefined);
105+
});
106+
107+
test.serial("that `undefined` is returned when no supported CI provider is detected", async (t) => {
108+
td.when(envCi()).thenReturn({ name: "Other Service" });
109+
110+
t.is(await exchangeToken(pkg), undefined);
111+
});

0 commit comments

Comments
 (0)