Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: respect registry-scoped certfile and keyfile options #125

Merged
merged 1 commit into from
Jul 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions lib/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict'
const fs = require('fs')
const npa = require('npm-package-arg')
const { URL } = require('url')

Expand All @@ -7,7 +8,8 @@ const { URL } = require('url')
const regKeyFromURI = (uri, opts) => {
const parsed = new URL(uri)
// try to find a config key indicating we have auth for this registry
// can be one of :_authToken, :_auth, or :_password and :username
// can be one of :_authToken, :_auth, :_password and :username, or
// :certfile and :keyfile
// We walk up the "path" until we're left with just //<host>[:<port>],
// stopping when we reach '//'.
let regKey = `//${parsed.host}${parsed.pathname}`
Expand All @@ -26,7 +28,8 @@ const regKeyFromURI = (uri, opts) => {
const hasAuth = (regKey, opts) => (
opts[`${regKey}:_authToken`] ||
opts[`${regKey}:_auth`] ||
opts[`${regKey}:username`] && opts[`${regKey}:_password`]
opts[`${regKey}:username`] && opts[`${regKey}:_password`] ||
opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`]
)

const sameHost = (a, b) => {
Expand All @@ -44,6 +47,17 @@ const getRegistry = opts => {
return scopeReg || opts.registry
}

const maybeReadFile = file => {
try {
return fs.readFileSync(file, 'utf8')
} catch (er) {
wraithgar marked this conversation as resolved.
Show resolved Hide resolved
if (er.code !== 'ENOENT') {
throw er
}
return null
}
}

const getAuth = (uri, opts = {}) => {
const { forceAuth } = opts
if (!uri) {
Expand All @@ -59,6 +73,8 @@ const getAuth = (uri, opts = {}) => {
username: forceAuth.username,
password: forceAuth._password || forceAuth.password,
auth: forceAuth._auth || forceAuth.auth,
certfile: forceAuth.certfile,
keyfile: forceAuth.keyfile,
})
}

Expand All @@ -82,6 +98,8 @@ const getAuth = (uri, opts = {}) => {
[`${regKey}:username`]: username,
[`${regKey}:_password`]: password,
[`${regKey}:_auth`]: auth,
[`${regKey}:certfile`]: certfile,
[`${regKey}:keyfile`]: keyfile,
} = opts

return new Auth({
Expand All @@ -90,15 +108,19 @@ const getAuth = (uri, opts = {}) => {
auth,
username,
password,
certfile,
keyfile,
})
}

class Auth {
constructor ({ token, auth, username, password, scopeAuthKey }) {
constructor ({ token, auth, username, password, scopeAuthKey, certfile, keyfile }) {
this.scopeAuthKey = scopeAuthKey
this.token = null
this.auth = null
this.isBasicAuth = false
this.cert = null
this.key = null
if (token) {
this.token = token
} else if (auth) {
Expand All @@ -108,6 +130,15 @@ class Auth {
this.auth = Buffer.from(`${username}:${p}`, 'utf8').toString('base64')
this.isBasicAuth = true
}
// mTLS may be used in conjunction with another auth method above
if (certfile && keyfile) {
const cert = maybeReadFile(certfile, 'utf-8')
const key = maybeReadFile(keyfile, 'utf-8')
if (cert && key) {
this.cert = cert
this.key = key
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ function regFetch (uri, /* istanbul ignore next */ opts_ = {}) {
cache: getCacheMode(opts),
cachePath: opts.cache,
ca: opts.ca,
cert: opts.cert,
cert: auth.cert || opts.cert,
headers,
integrity: opts.integrity,
key: opts.key,
key: auth.key || opts.key,
localAddress: opts.localAddress,
maxSockets: opts.maxSockets,
memoize: opts.memoize,
Expand Down
86 changes: 86 additions & 0 deletions test/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ t.test('basic auth', t => {
token: null,
isBasicAuth: true,
auth: Buffer.from('user:pass').toString('base64'),
cert: null,
key: null,
}, 'basic auth details generated')

const opts = Object.assign({}, OPTS, config)
Expand Down Expand Up @@ -62,6 +64,8 @@ t.test('token auth', t => {
isBasicAuth: false,
token: 'c0ffee',
auth: null,
cert: null,
key: null,
}, 'correct auth token picked out')

const opts = Object.assign({}, OPTS, config)
Expand All @@ -77,24 +81,37 @@ t.test('token auth', t => {
})

t.test('forceAuth', t => {
const dir = t.testdir({
'my.cert': 'my cert',
'my.key': 'my key',
'other.cert': 'other cert',
'other.key': 'other key',
})

const config = {
registry: 'https://my.custom.registry/here/',
token: 'deadbeef',
'always-auth': false,
'//my.custom.registry/here/:_authToken': 'c0ffee',
'//my.custom.registry/here/:token': 'nope',
'//my.custom.registry/here/:certfile': `${dir}/my.cert`,
'//my.custom.registry/here/:keyfile': `${dir}/my.key`,
forceAuth: {
username: 'user',
password: Buffer.from('pass', 'utf8').toString('base64'),
email: '[email protected]',
'always-auth': true,
certfile: `${dir}/other.cert`,
keyfile: `${dir}/other.key`,
},
}
t.same(getAuth(config.registry, config), {
scopeAuthKey: null,
token: null,
isBasicAuth: true,
auth: Buffer.from('user:pass').toString('base64'),
cert: 'other cert',
key: 'other key',
}, 'only forceAuth details included')

const opts = Object.assign({}, OPTS, config)
Expand Down Expand Up @@ -126,6 +143,8 @@ t.test('forceAuth token', t => {
isBasicAuth: false,
token: 'cafebad',
auth: null,
cert: null,
key: null,
}, 'correct forceAuth token picked out')

const opts = Object.assign({}, OPTS, config)
Expand All @@ -152,6 +171,8 @@ t.test('_auth auth', t => {
token: null,
isBasicAuth: false,
auth: 'c0ffee',
cert: null,
key: null,
}, 'correct _auth picked out')

const opts = Object.assign({}, OPTS, config)
Expand All @@ -177,6 +198,8 @@ t.test('_auth username:pass auth', t => {
token: null,
isBasicAuth: false,
auth: auth,
cert: null,
key: null,
}, 'correct _auth picked out')

const opts = Object.assign({}, OPTS, config)
Expand Down Expand Up @@ -226,6 +249,8 @@ t.test('globally-configured auth', t => {
token: null,
isBasicAuth: true,
auth: Buffer.from('globaluser:globalpass').toString('base64'),
cert: null,
key: null,
}, 'basic auth details generated from global settings')

const tokenConfig = {
Expand All @@ -239,6 +264,8 @@ t.test('globally-configured auth', t => {
token: 'deadbeef',
isBasicAuth: false,
auth: null,
cert: null,
key: null,
}, 'correct global auth token picked out')

const _authConfig = {
Expand All @@ -252,6 +279,8 @@ t.test('globally-configured auth', t => {
token: null,
isBasicAuth: false,
auth: 'deadbeef',
cert: null,
key: null,
}, 'correct _auth picked out')

t.end()
Expand All @@ -270,6 +299,8 @@ t.test('otp token passed through', t => {
token: 'c0ffee',
isBasicAuth: false,
auth: null,
cert: null,
key: null,
}, 'correct auth token picked out')

const opts = Object.assign({}, OPTS, config)
Expand Down Expand Up @@ -337,6 +368,8 @@ t.test('always-auth', t => {
token: 'c0ffee',
isBasicAuth: false,
auth: null,
cert: null,
key: null,
}, 'correct auth token picked out')

const opts = Object.assign({}, OPTS, config)
Expand All @@ -349,25 +382,36 @@ t.test('always-auth', t => {
})

t.test('scope-based auth', t => {
const dir = t.testdir({
'my.cert': 'my cert',
'my.key': 'my key',
})

const config = {
registry: 'https://my.custom.registry/here/',
scope: '@myscope',
'@myscope:registry': 'https://my.custom.registry/here/',
token: 'deadbeef',
'//my.custom.registry/here/:_authToken': 'c0ffee',
'//my.custom.registry/here/:token': 'nope',
'//my.custom.registry/here/:certfile': `${dir}/my.cert`,
'//my.custom.registry/here/:keyfile': `${dir}/my.key`,
}
t.same(getAuth(config['@myscope:registry'], config), {
scopeAuthKey: null,
auth: null,
isBasicAuth: false,
token: 'c0ffee',
cert: 'my cert',
key: 'my key',
}, 'correct auth token picked out')
t.same(getAuth(config['@myscope:registry'], config), {
scopeAuthKey: null,
auth: null,
isBasicAuth: false,
token: 'c0ffee',
cert: 'my cert',
key: 'my key',
}, 'correct auth token picked out without scope config having an @')

const opts = Object.assign({}, OPTS, config)
Expand All @@ -392,6 +436,32 @@ t.test('auth needs a uri', t => {
t.end()
})

t.test('certfile and keyfile errors', t => {
const dir = t.testdir({
'my.cert': 'my cert',
})

t.same(getAuth('https://my.custom.registry/here/', {
'//my.custom.registry/here/:certfile': `${dir}/my.cert`,
'//my.custom.registry/here/:keyfile': `${dir}/nosuch.key`,
}), {
scopeAuthKey: null,
auth: null,
isBasicAuth: false,
token: null,
cert: null,
key: null,
}, 'cert and key ignored if one doesn\'t exist')

t.throws(() => {
getAuth('https://my.custom.registry/here/', {
'//my.custom.registry/here/:certfile': `${dir}/my.cert`,
'//my.custom.registry/here/:keyfile': dir,
})
}, /EISDIR/, 'other read errors are propagated')
t.end()
})

t.test('do not be thrown by other weird configs', t => {
const opts = {
scope: '@asdf',
Expand All @@ -412,6 +482,8 @@ t.test('do not be thrown by other weird configs', t => {
token: 'correct bearer token',
isBasicAuth: false,
auth: null,
cert: null,
key: null,
})
t.end()
})
Expand All @@ -430,27 +502,35 @@ t.test('scopeAuthKey tests', t => {
auth: null,
isBasicAuth: false,
token: null,
cert: null,
key: null,
}, 'regular scoped spec')

t.same(getAuth(uri, { ...opts, spec: 'foo@npm:@scope/foo@latest' }), {
scopeAuthKey: '//scope-host.com/',
auth: null,
isBasicAuth: false,
token: null,
cert: null,
key: null,
}, 'scoped pkg aliased to unscoped name')

t.same(getAuth(uri, { ...opts, spec: '@other-scope/foo@npm:@scope/foo@latest' }), {
scopeAuthKey: '//scope-host.com/',
auth: null,
isBasicAuth: false,
token: null,
cert: null,
key: null,
}, 'scoped name aliased to other scope with auth')

t.same(getAuth(uri, { ...opts, spec: '@scope/foo@npm:foo@latest' }), {
scopeAuthKey: null,
auth: null,
isBasicAuth: false,
token: null,
cert: null,
key: null,
}, 'unscoped aliased to scoped name')

t.end()
Expand All @@ -470,18 +550,24 @@ t.test('registry host matches, path does not, send auth', t => {
token: 'c0ffee',
auth: null,
isBasicAuth: false,
cert: null,
key: null,
})
t.same(getAuth(uri, { ...opts, spec: '@other-scope/foo' }), {
scopeAuthKey: '//other-scope-registry.com/other/scope/',
token: null,
auth: null,
isBasicAuth: false,
cert: null,
key: null,
})
t.same(getAuth(uri, { ...opts, registry: 'https://scope-host.com/scope/host/' }), {
scopeAuthKey: null,
token: 'c0ffee',
auth: null,
isBasicAuth: false,
cert: null,
key: null,
})
t.end()
})