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

gar/auth regkey #212

Merged
merged 2 commits into from
Oct 10, 2023
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
64 changes: 50 additions & 14 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const npa = require('npm-package-arg')
const { URL } = require('url')

// Find the longest registry key that is used for some kind of auth
// in the options.
const regKeyFromURI = (uri, opts) => {
// in the options. Returns the registry key and the auth config.
const regFromURI = (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, :_password and :username, or
Expand All @@ -14,23 +14,40 @@ const regKeyFromURI = (uri, opts) => {
// stopping when we reach '//'.
let regKey = `//${parsed.host}${parsed.pathname}`
while (regKey.length > '//'.length) {
const authKey = hasAuth(regKey, opts)
// got some auth for this URI
if (hasAuth(regKey, opts)) {
return regKey
if (authKey) {
return { regKey, authKey }
}

// can be either //host/some/path/:_auth or //host/some/path:_auth
// walk up by removing EITHER what's after the slash OR the slash itself
regKey = regKey.replace(/([^/]+|\/)$/, '')
}
return { regKey: false, authKey: null }
}

const hasAuth = (regKey, opts) => (
opts[`${regKey}:_authToken`] ||
opts[`${regKey}:_auth`] ||
opts[`${regKey}:username`] && opts[`${regKey}:_password`] ||
opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`]
)
// Not only do we want to know if there is auth, but if we are calling `npm
// logout` we want to know what config value specifically provided it. This is
// so we can look up where the config came from to delete it (i.e. user vs
// project)
const hasAuth = (regKey, opts) => {
if (opts[`${regKey}:_authToken`]) {
return '_authToken'
}
if (opts[`${regKey}:_auth`]) {
return '_auth'
}
if (opts[`${regKey}:username`] && opts[`${regKey}:_password`]) {
// 'password' can be inferred to also be present
return 'username'
}
if (opts[`${regKey}:certfile`] && opts[`${regKey}:keyfile`]) {
// 'keyfile' can be inferred to also be present
return 'certfile'
}
return false
}

const sameHost = (a, b) => {
const parsedA = new URL(a)
Expand Down Expand Up @@ -63,11 +80,14 @@ const getAuth = (uri, opts = {}) => {
if (!uri) {
throw new Error('URI is required')
}
const regKey = regKeyFromURI(uri, forceAuth || opts)
const { regKey, authKey } = regFromURI(uri, forceAuth || opts)

// we are only allowed to use what's in forceAuth if specified
if (forceAuth && !regKey) {
return new Auth({
// if we force auth we don't want to refer back to anything in config
regKey: false,
authKey: null,
scopeAuthKey: null,
token: forceAuth._authToken || forceAuth.token,
username: forceAuth.username,
Expand All @@ -88,8 +108,8 @@ const getAuth = (uri, opts = {}) => {
// registry where we logged in, but the same auth SHOULD be sent
// to that artifact host, then we track where it was coming in from,
// and warn the user if we get a 4xx error on it.
const scopeAuthKey = regKeyFromURI(registry, opts)
return new Auth({ scopeAuthKey })
const { regKey: scopeAuthKey, authKey: _authKey } = regFromURI(registry, opts)
return new Auth({ scopeAuthKey, regKey: scopeAuthKey, authKey: _authKey })
}
}

Expand All @@ -104,6 +124,8 @@ const getAuth = (uri, opts = {}) => {

return new Auth({
scopeAuthKey: null,
regKey,
authKey,
token,
auth,
username,
Expand All @@ -114,8 +136,22 @@ const getAuth = (uri, opts = {}) => {
}

class Auth {
constructor ({ token, auth, username, password, scopeAuthKey, certfile, keyfile }) {
constructor ({
token,
auth,
username,
password,
scopeAuthKey,
certfile,
keyfile,
regKey,
authKey,
}) {
// same as regKey but only present for scoped auth. Should have been named scopeRegKey
this.scopeAuthKey = scopeAuthKey
// `${regKey}:${authKey}` will get you back to the auth config that gave us auth
this.regKey = regKey
this.authKey = authKey
this.token = null
this.auth = null
this.isBasicAuth = false
Expand Down
2 changes: 2 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ function regFetch (uri, /* istanbul ignore next */ opts_ = {}) {
return Promise.resolve(body).then(doFetch)
}

module.exports.getAuth = getAuth

module.exports.json = fetchJSON
function fetchJSON (uri, opts) {
return regFetch(uri, opts).then(res => res.json())
Expand Down
48 changes: 48 additions & 0 deletions test/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ t.test('basic auth', t => {
const gotAuth = getAuth(config.registry, config)
t.same(gotAuth, {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: 'username',
token: null,
isBasicAuth: true,
auth: Buffer.from('user:pass').toString('base64'),
Expand Down Expand Up @@ -61,6 +63,8 @@ t.test('token auth', t => {
}
t.same(getAuth(`${config.registry}/foo/-/foo.tgz`, config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_authToken',
isBasicAuth: false,
token: 'c0ffee',
auth: null,
Expand Down Expand Up @@ -107,6 +111,8 @@ t.test('forceAuth', t => {
}
t.same(getAuth(config.registry, config), {
scopeAuthKey: null,
regKey: false,
authKey: null,
token: null,
isBasicAuth: true,
auth: Buffer.from('user:pass').toString('base64'),
Expand Down Expand Up @@ -140,6 +146,8 @@ t.test('forceAuth token', t => {
}
t.same(getAuth(config.registry, config), {
scopeAuthKey: null,
regKey: false,
authKey: null,
isBasicAuth: false,
token: 'cafebad',
auth: null,
Expand Down Expand Up @@ -168,6 +176,8 @@ t.test('_auth auth', t => {
}
t.same(getAuth(`${config.registry}/asdf/foo/bar/baz`, config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_auth',
token: null,
isBasicAuth: false,
auth: 'c0ffee',
Expand Down Expand Up @@ -195,6 +205,8 @@ t.test('_auth username:pass auth', t => {
}
t.same(getAuth(config.registry, config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_auth',
token: null,
isBasicAuth: false,
auth: auth,
Expand Down Expand Up @@ -246,6 +258,8 @@ t.test('globally-configured auth', t => {
}
t.same(getAuth(basicConfig.registry, basicConfig), {
scopeAuthKey: null,
regKey: '//different.registry/',
authKey: 'username',
token: null,
isBasicAuth: true,
auth: Buffer.from('globaluser:globalpass').toString('base64'),
Expand All @@ -261,6 +275,8 @@ t.test('globally-configured auth', t => {
}
t.same(getAuth(tokenConfig.registry, tokenConfig), {
scopeAuthKey: null,
regKey: '//different.registry/',
authKey: '_authToken',
token: 'deadbeef',
isBasicAuth: false,
auth: null,
Expand All @@ -276,6 +292,8 @@ t.test('globally-configured auth', t => {
}
t.same(getAuth(`${_authConfig.registry}/foo`, _authConfig), {
scopeAuthKey: null,
regKey: '//different.registry',
authKey: '_auth',
token: null,
isBasicAuth: false,
auth: 'deadbeef',
Expand All @@ -296,6 +314,8 @@ t.test('otp token passed through', t => {
}
t.same(getAuth(config.registry, config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_authToken',
token: 'c0ffee',
isBasicAuth: false,
auth: null,
Expand Down Expand Up @@ -365,6 +385,8 @@ t.test('always-auth', t => {
}
t.same(getAuth(config.registry, config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_authToken',
token: 'c0ffee',
isBasicAuth: false,
auth: null,
Expand Down Expand Up @@ -399,6 +421,8 @@ t.test('scope-based auth', t => {
}
t.same(getAuth(config['@myscope:registry'], config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_authToken',
auth: null,
isBasicAuth: false,
token: 'c0ffee',
Expand All @@ -407,6 +431,8 @@ t.test('scope-based auth', t => {
}, 'correct auth token picked out')
t.same(getAuth(config['@myscope:registry'], config), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: '_authToken',
auth: null,
isBasicAuth: false,
token: 'c0ffee',
Expand Down Expand Up @@ -446,6 +472,8 @@ t.test('certfile and keyfile errors', t => {
'//my.custom.registry/here/:keyfile': `${dir}/nosuch.key`,
}), {
scopeAuthKey: null,
regKey: '//my.custom.registry/here/',
authKey: 'certfile',
auth: null,
isBasicAuth: false,
token: null,
Expand Down Expand Up @@ -479,6 +507,8 @@ t.test('do not be thrown by other weird configs', t => {
const auth = getAuth(uri, opts)
t.same(auth, {
scopeAuthKey: null,
regKey: '//localhost:15443/foo',
authKey: '_authToken',
token: 'correct bearer token',
isBasicAuth: false,
auth: null,
Expand All @@ -499,6 +529,8 @@ t.test('scopeAuthKey tests', t => {

t.same(getAuth(uri, { ...opts, spec: '@scope/foo@latest' }), {
scopeAuthKey: '//scope-host.com/',
regKey: '//scope-host.com/',
authKey: '_authToken',
auth: null,
isBasicAuth: false,
token: null,
Expand All @@ -508,6 +540,8 @@ t.test('scopeAuthKey tests', t => {

t.same(getAuth(uri, { ...opts, spec: 'foo@npm:@scope/foo@latest' }), {
scopeAuthKey: '//scope-host.com/',
regKey: '//scope-host.com/',
authKey: '_authToken',
auth: null,
isBasicAuth: false,
token: null,
Expand All @@ -517,6 +551,8 @@ t.test('scopeAuthKey tests', t => {

t.same(getAuth(uri, { ...opts, spec: '@other-scope/foo@npm:@scope/foo@latest' }), {
scopeAuthKey: '//scope-host.com/',
regKey: '//scope-host.com/',
authKey: '_authToken',
auth: null,
isBasicAuth: false,
token: null,
Expand All @@ -526,6 +562,8 @@ t.test('scopeAuthKey tests', t => {

t.same(getAuth(uri, { ...opts, spec: '@scope/foo@npm:foo@latest' }), {
scopeAuthKey: null,
regKey: false,
authKey: null,
auth: null,
isBasicAuth: false,
token: null,
Expand All @@ -547,6 +585,8 @@ t.test('registry host matches, path does not, send auth', t => {
const uri = 'https://scope-host.com/blahblah/bloobloo/foo.tgz'
t.same(getAuth(uri, { ...opts, spec: '@scope/foo' }), {
scopeAuthKey: null,
regKey: '//scope-host.com/scope/host/',
authKey: '_authToken',
token: 'c0ffee',
auth: null,
isBasicAuth: false,
Expand All @@ -555,6 +595,8 @@ t.test('registry host matches, path does not, send auth', t => {
})
t.same(getAuth(uri, { ...opts, spec: '@other-scope/foo' }), {
scopeAuthKey: '//other-scope-registry.com/other/scope/',
regKey: '//other-scope-registry.com/other/scope/',
authKey: '_authToken',
token: null,
auth: null,
isBasicAuth: false,
Expand All @@ -563,6 +605,8 @@ t.test('registry host matches, path does not, send auth', t => {
})
t.same(getAuth(uri, { ...opts, registry: 'https://scope-host.com/scope/host/' }), {
scopeAuthKey: null,
regKey: '//scope-host.com/scope/host/',
authKey: '_authToken',
token: 'c0ffee',
auth: null,
isBasicAuth: false,
Expand All @@ -571,3 +615,7 @@ t.test('registry host matches, path does not, send auth', t => {
})
t.end()
})

t.test('getAuth is exported', async t => {
t.equal(fetch.getAuth, getAuth)
})