From 84823a5c85b010f62d9a9f3b890fdee0d1d6f80a Mon Sep 17 00:00:00 2001 From: Gar Date: Tue, 10 Oct 2023 09:36:23 -0700 Subject: [PATCH] feat: include regKey and authKey in auth object This will allow consumers to know *what* auth value provided the auth --- lib/auth.js | 64 ++++++++++++++++++++++++++++++++++++++++------------ test/auth.js | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/lib/auth.js b/lib/auth.js index 870ce0d9..9270025f 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -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 @@ -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) @@ -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, @@ -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 }) } } @@ -104,6 +124,8 @@ const getAuth = (uri, opts = {}) => { return new Auth({ scopeAuthKey: null, + regKey, + authKey, token, auth, username, @@ -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 diff --git a/test/auth.js b/test/auth.js index 5e88f312..ac658226 100644 --- a/test/auth.js +++ b/test/auth.js @@ -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'), @@ -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, @@ -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'), @@ -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, @@ -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', @@ -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, @@ -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'), @@ -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, @@ -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', @@ -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, @@ -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, @@ -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', @@ -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', @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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, @@ -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,