Skip to content

Commit 5ef53ee

Browse files
authored
feat: accept registry-scoped certfile and keyfile as credentials (#5160)
Closes #4765 RFC: npm/rfcs#591 While this doesn't directly allow top-level cert/key as credentials (per the original issue), it's a more targeted/secure approach that accomplishes the same end-result; the new options are scoped to a specific registry, and the actual cert/key contents are much less likely to be exposed. See the RFC for more context. Depends on: * npm/npm-registry-fetch#125 * npm/config#69
1 parent 51b12a0 commit 5ef53ee

File tree

9 files changed

+71
-17
lines changed

9 files changed

+71
-17
lines changed

docs/content/using-npm/config.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -357,8 +357,9 @@ newlines replaced by the string "\n". For example:
357357
cert="-----BEGIN CERTIFICATE-----\nXXXX\nXXXX\n-----END CERTIFICATE-----"
358358
```
359359

360-
It is _not_ the path to a certificate file (and there is no "certfile"
361-
option).
360+
It is _not_ the path to a certificate file, though you can set a
361+
registry-scoped "certfile" path like
362+
"//other-registry.tld/:certfile=/path/to/cert.pem".
362363

363364
<!-- automatically generated, do not edit manually -->
364365
<!-- see lib/utils/config/definitions.js -->
@@ -946,7 +947,8 @@ format with newlines replaced by the string "\n". For example:
946947
key="-----BEGIN PRIVATE KEY-----\nXXXX\nXXXX\n-----END PRIVATE KEY-----"
947948
```
948949

949-
It is _not_ the path to a key file (and there is no "keyfile" option).
950+
It is _not_ the path to a key file, though you can set a registry-scoped
951+
"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem".
950952

951953
<!-- automatically generated, do not edit manually -->
952954
<!-- see lib/utils/config/definitions.js -->

lib/commands/publish.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ class Publish extends BaseCommand {
101101
const resolved = npa.resolve(manifest.name, manifest.version)
102102
const registry = npmFetch.pickRegistry(resolved, opts)
103103
const creds = this.npm.config.getCredentialsByURI(registry)
104-
const noCreds = !creds.token && !creds.username
104+
const noCreds = !(creds.token || creds.username || creds.certfile && creds.keyfile)
105105
const outputRegistry = replaceInfo(registry)
106106

107107
if (noCreds) {

lib/utils/config/definitions.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -436,8 +436,8 @@ define('cert', {
436436
cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----"
437437
\`\`\`
438438
439-
It is _not_ the path to a certificate file (and there is no "certfile"
440-
option).
439+
It is _not_ the path to a certificate file, though you can set a registry-scoped
440+
"certfile" path like "//other-registry.tld/:certfile=/path/to/cert.pem".
441441
`,
442442
flatten,
443443
})
@@ -1118,7 +1118,8 @@ define('key', {
11181118
key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----"
11191119
\`\`\`
11201120
1121-
It is _not_ the path to a key file (and there is no "keyfile" option).
1121+
It is _not_ the path to a key file, though you can set a registry-scoped
1122+
"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem".
11221123
`,
11231124
flatten,
11241125
})

lib/utils/get-identity.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ module.exports = async (npm, opts) => {
99
return creds.username
1010
}
1111

12-
// No username, but we have a token; fetch the username from registry
13-
if (creds.token) {
12+
// No username, but we have other credentials; fetch the username from registry
13+
if (creds.token || creds.certfile && creds.keyfile) {
1414
const registryData = await npmFetch.json('/-/whoami', { ...opts })
1515
return registryData.username
1616
}

tap-snapshots/test/lib/commands/publish.js.test.cjs

+5-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,11 @@ Array [
5656
]
5757
`
5858

59-
exports[`test/lib/commands/publish.js TAP has auth for scope configured registry > new package version 1`] = `
59+
exports[`test/lib/commands/publish.js TAP has mTLS auth for scope configured registry > new package version 1`] = `
60+
61+
`
62+
63+
exports[`test/lib/commands/publish.js TAP has token auth for scope configured registry > new package version 1`] = `
6064
6165
`
6266

tap-snapshots/test/lib/utils/config/definitions.js.test.cjs

+5-3
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,9 @@ newlines replaced by the string "\\n". For example:
404404
cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----"
405405
\`\`\`
406406
407-
It is _not_ the path to a certificate file (and there is no "certfile"
408-
option).
407+
It is _not_ the path to a certificate file, though you can set a
408+
registry-scoped "certfile" path like
409+
"//other-registry.tld/:certfile=/path/to/cert.pem".
409410
`
410411

411412
exports[`test/lib/utils/config/definitions.js TAP > config description for ci-name 1`] = `
@@ -1016,7 +1017,8 @@ format with newlines replaced by the string "\\n". For example:
10161017
key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----"
10171018
\`\`\`
10181019
1019-
It is _not_ the path to a key file (and there is no "keyfile" option).
1020+
It is _not_ the path to a key file, though you can set a registry-scoped
1021+
"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem".
10201022
`
10211023

10221024
exports[`test/lib/utils/config/definitions.js TAP > config description for legacy-bundling 1`] = `

tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs

+5-3
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ newlines replaced by the string "\\n". For example:
230230
cert="-----BEGIN CERTIFICATE-----\\nXXXX\\nXXXX\\n-----END CERTIFICATE-----"
231231
\`\`\`
232232
233-
It is _not_ the path to a certificate file (and there is no "certfile"
234-
option).
233+
It is _not_ the path to a certificate file, though you can set a
234+
registry-scoped "certfile" path like
235+
"//other-registry.tld/:certfile=/path/to/cert.pem".
235236
236237
<!-- automatically generated, do not edit manually -->
237238
<!-- see lib/utils/config/definitions.js -->
@@ -819,7 +820,8 @@ format with newlines replaced by the string "\\n". For example:
819820
key="-----BEGIN PRIVATE KEY-----\\nXXXX\\nXXXX\\n-----END PRIVATE KEY-----"
820821
\`\`\`
821822
822-
It is _not_ the path to a key file (and there is no "keyfile" option).
823+
It is _not_ the path to a key file, though you can set a registry-scoped
824+
"keyfile" path like "//other-registry.tld/:keyfile=/path/to/key.pem".
823825
824826
<!-- automatically generated, do not edit manually -->
825827
<!-- see lib/utils/config/definitions.js -->

test/lib/commands/publish.js

+30-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ t.test('no auth for scope configured registry', async t => {
327327
)
328328
})
329329

330-
t.test('has auth for scope configured registry', async t => {
330+
t.test('has token auth for scope configured registry', async t => {
331331
const spec = npa('@npm/test-package')
332332
const { npm, joinedOutput } = await loadMockNpm(t, {
333333
config: {
@@ -356,6 +356,35 @@ t.test('has auth for scope configured registry', async t => {
356356
t.matchSnapshot(joinedOutput(), 'new package version')
357357
})
358358

359+
t.test('has mTLS auth for scope configured registry', async t => {
360+
const spec = npa('@npm/test-package')
361+
const { npm, joinedOutput } = await loadMockNpm(t, {
362+
config: {
363+
'@npm:registry': alternateRegistry,
364+
[`${alternateRegistry.slice(6)}/:certfile`]: '/some.cert',
365+
[`${alternateRegistry.slice(6)}/:keyfile`]: '/some.key',
366+
},
367+
prefixDir: {
368+
'package.json': JSON.stringify({
369+
name: '@npm/test-package',
370+
version: '1.0.0',
371+
}, null, 2),
372+
},
373+
globals: ({ prefix }) => ({
374+
'process.cwd': () => prefix,
375+
}),
376+
})
377+
const registry = new MockRegistry({
378+
tap: t,
379+
registry: alternateRegistry,
380+
})
381+
registry.nock.put(`/${spec.escapedName}`, body => {
382+
return t.match(body, { name: '@npm/test-package' })
383+
}).reply(200, {})
384+
await npm.exec('publish', [])
385+
t.matchSnapshot(joinedOutput(), 'new package version')
386+
})
387+
359388
t.test('workspaces', t => {
360389
const dir = {
361390
'package.json': JSON.stringify(

test/lib/commands/whoami.js

+14
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ t.test('npm whoami --json', async t => {
3434
t.equal(JSON.parse(joinedOutput()), username, 'should print username')
3535
})
3636

37+
t.test('npm whoami using mTLS', async t => {
38+
const { npm, joinedOutput } = await loadMockNpm(t, { config: {
39+
'//registry.npmjs.org/:certfile': '/some.cert',
40+
'//registry.npmjs.org/:keyfile': '/some.key',
41+
} })
42+
const registry = new MockRegistry({
43+
tap: t,
44+
registry: npm.config.get('registry'),
45+
})
46+
registry.whoami({ username })
47+
await npm.exec('whoami', [])
48+
t.equal(joinedOutput(), username, 'should print username')
49+
})
50+
3751
t.test('credentials from token', async t => {
3852
const { npm, joinedOutput } = await loadMockNpm(t, {
3953
config: {

0 commit comments

Comments
 (0)