diff --git a/docs/content/commands/npm-adduser.md b/docs/content/commands/npm-adduser.md index 700aecb2255b2..91f1195afbbee 100644 --- a/docs/content/commands/npm-adduser.md +++ b/docs/content/commands/npm-adduser.md @@ -13,7 +13,7 @@ description: Add a registry user account ```bash npm adduser -aliases: login, add-user +alias: add-user ``` @@ -25,22 +25,12 @@ Note: This command is unaware of workspaces. ### Description -Create or verify a user named `` in the specified registry, and -save the credentials to the `.npmrc` file. If no registry is specified, -the default registry will be used (see [`config`](/using-npm/config)). +Create a new user in the specified registry, and save the credentials to +the `.npmrc` file. If no registry is specified, the default registry +will be used (see [`config`](/using-npm/config)). -The username, password, and email are read in from prompts. - -To reset your password, go to - -To change your email address, go to - -You may use this command multiple times with the same user account to -authorize on a new machine. When authenticating on a new machine, -the username, password and email address must all match with -your existing record. - -`npm login` is an alias to `adduser` and behaves exactly the same way. +When using `legacy` for your `auth-type`, the username, password, and +email are read in from prompts. ### Configuration @@ -93,10 +83,7 @@ npm init --scope=@foo --yes #### `auth-type` * Default: "legacy" -* Type: "legacy", "web", "sso", "saml", "oauth", or "webauthn" - -NOTE: auth-type values "sso", "saml", "oauth", and "webauthn" will be -removed in a future version. +* Type: "legacy" or "web" What authentication strategy to use with `login`. diff --git a/docs/content/commands/npm-login.md b/docs/content/commands/npm-login.md new file mode 100644 index 0000000000000..4f821c903b168 --- /dev/null +++ b/docs/content/commands/npm-login.md @@ -0,0 +1,110 @@ +--- +title: npm-login +section: 1 +description: Login to a registry user account +--- + +### Synopsis + + + + + +```bash +npm login +``` + + + + + + +Note: This command is unaware of workspaces. + +### Description + +Verify a user in the specified registry, and save the credentials to the +`.npmrc` file. If no registry is specified, the default registry will be +used (see [`config`](/using-npm/config)). + +When using `legacy` for your `auth-type`, the username and password, are +read in from prompts. + +To reset your password, go to + +To change your email address, go to + +You may use this command multiple times with the same user account to +authorize on a new machine. When authenticating on a new machine, +the username, password and email address must all match with +your existing record. + +### Configuration + + + + +#### `registry` + +* Default: "https://registry.npmjs.org/" +* Type: URL + +The base URL of the npm registry. + + + + +#### `scope` + +* Default: the scope of the current project, if any, or "" +* Type: String + +Associate an operation with a scope for a scoped registry. + +Useful when logging in to or out of a private registry: + +``` +# log in, linking the scope to the custom registry +npm login --scope=@mycorp --registry=https://registry.mycorp.com + +# log out, removing the link and the auth token +npm logout --scope=@mycorp +``` + +This will cause `@mycorp` to be mapped to the registry for future +installation of packages specified according to the pattern +`@mycorp/package`. + +This will also cause `npm init` to create a scoped package. + +``` +# accept all defaults, and create a package named "@foo/whatever", +# instead of just named "whatever" +npm init --scope=@foo --yes +``` + + + + + +#### `auth-type` + +* Default: "legacy" +* Type: "legacy" or "web" + +What authentication strategy to use with `login`. + + + + + + +### See Also + +* [npm registry](/using-npm/registry) +* [npm config](/commands/npm-config) +* [npmrc](/configuring-npm/npmrc) +* [npm owner](/commands/npm-owner) +* [npm whoami](/commands/npm-whoami) +* [npm token](/commands/npm-token) +* [npm profile](/commands/npm-profile) diff --git a/docs/content/using-npm/config.md b/docs/content/using-npm/config.md index e6a7231861ba0..2b704bda2df4e 100644 --- a/docs/content/using-npm/config.md +++ b/docs/content/using-npm/config.md @@ -218,10 +218,7 @@ exit code. #### `auth-type` * Default: "legacy" -* Type: "legacy", "web", "sso", "saml", "oauth", or "webauthn" - -NOTE: auth-type values "sso", "saml", "oauth", and "webauthn" will be -removed in a future version. +* Type: "legacy" or "web" What authentication strategy to use with `login`. @@ -2084,31 +2081,6 @@ Alias for --package-lock -#### `sso-poll-frequency` - -* Default: 500 -* Type: Number -* DEPRECATED: The --auth-type method of SSO/SAML/OAuth will be removed in a - future version of npm in favor of web-based login. - -When used with SSO-enabled `auth-type`s, configures how regularly the -registry should be polled while the user is completing authentication. - - - - -#### `sso-type` - -* Default: "oauth" -* Type: null, "oauth", or "saml" -* DEPRECATED: The --auth-type method of SSO/SAML/OAuth will be removed in a - future version of npm in favor of web-based login. - -If `--auth-type=sso`, the type of SSO type to use. - - - - #### `tmp` * Default: The value returned by the Node.js `os.tmpdir()` method diff --git a/docs/nav.yml b/docs/nav.yml index 565537054a0d7..43051e4e9c3f1 100644 --- a/docs/nav.yml +++ b/docs/nav.yml @@ -96,6 +96,9 @@ - title: npm link url: /commands/npm-link description: Symlink a package folder + - title: npm login + url: /commands/npm-login + description: Login to a registry user account - title: npm logout url: /commands/npm-logout description: Log out of the registry diff --git a/lib/auth/legacy.js b/lib/auth/legacy.js deleted file mode 100644 index 9aed12f3926fb..0000000000000 --- a/lib/auth/legacy.js +++ /dev/null @@ -1,100 +0,0 @@ -const profile = require('npm-profile') -const log = require('../utils/log-shim') -const openUrlPrompt = require('../utils/open-url-prompt.js') -const read = require('../utils/read-user-info.js') - -const loginPrompter = async (creds) => { - creds.username = await read.username('Username:', creds.username) - creds.password = await read.password('Password:', creds.password) - creds.email = await read.email('Email: (this IS public) ', creds.email) - - return creds -} - -const login = async (npm, opts) => { - let res - - const requestOTP = async () => { - const otp = await read.otp( - 'Enter one-time password: ' - ) - - return profile.loginCouch( - opts.creds.username, - opts.creds.password, - { ...opts, otp } - ) - } - - const addNewUser = async () => { - let newUser - - try { - newUser = await profile.adduserCouch( - opts.creds.username, - opts.creds.email, - opts.creds.password, - opts - ) - } catch (err) { - if (err.code === 'EOTP') { - newUser = await requestOTP() - } else { - throw err - } - } - - return newUser - } - - const openerPromise = (url, emitter) => - openUrlPrompt( - npm, - url, - 'Authenticate your account at', - 'Press ENTER to open in the browser...', - emitter - ) - - try { - res = await profile.login(openerPromise, loginPrompter, opts) - } catch (err) { - const needsMoreInfo = !(opts && - opts.creds && - opts.creds.username && - opts.creds.password && - opts.creds.email) - if (err.code === 'EOTP') { - res = await requestOTP() - } else if (needsMoreInfo) { - throw err - } else { - // TODO: maybe this needs to check for err.code === 'E400' instead? - res = await addNewUser() - } - } - - const newCreds = {} - if (res && res.token) { - newCreds.token = res.token - } else { - newCreds.username = opts.creds.username - newCreds.password = opts.creds.password - newCreds.email = opts.creds.email - newCreds.alwaysAuth = opts.creds.alwaysAuth - } - - const usermsg = opts.creds.username ? ` user ${opts.creds.username}` : '' - const scopeMessage = opts.scope ? ` to scope ${opts.scope}` : '' - const userout = opts.creds.username ? ` as ${opts.creds.username}` : '' - const message = `Logged in${userout}${scopeMessage} on ${opts.registry}.` - - log.info('login', `Authorized${usermsg}`) - - return { - message, - newCreds, - } -} - -module.exports = login diff --git a/lib/auth/oauth.js b/lib/auth/oauth.js deleted file mode 100644 index 99c2ca0ca04b7..0000000000000 --- a/lib/auth/oauth.js +++ /dev/null @@ -1,8 +0,0 @@ -const sso = require('./sso.js') - -const login = (npm, opts) => { - npm.config.set('sso-type', 'oauth') - return sso(npm, opts) -} - -module.exports = login diff --git a/lib/auth/saml.js b/lib/auth/saml.js deleted file mode 100644 index 3dd31ca013f52..0000000000000 --- a/lib/auth/saml.js +++ /dev/null @@ -1,8 +0,0 @@ -const sso = require('./sso.js') - -const login = (npm, opts) => { - npm.config.set('sso-type', 'saml') - return sso(npm, opts) -} - -module.exports = login diff --git a/lib/auth/sso.js b/lib/auth/sso.js deleted file mode 100644 index 621ead5d21b65..0000000000000 --- a/lib/auth/sso.js +++ /dev/null @@ -1,81 +0,0 @@ -// XXX: To date, npm Enterprise Legacy is the only system that ever -// implemented support for this type of login. A better way to do -// SSO is to use the WebLogin type of login supported by the npm-login -// module. This more forward-looking login style is, ironically, -// supported by the '--auth-type=legacy' type of login. -// When and if npm Enterprise Legacy is no longer supported by the npm -// CLI, we can remove this, and fold the lib/auth/legacy.js back into -// lib/adduser.js - -const profile = require('npm-profile') -const npmFetch = require('npm-registry-fetch') -const log = require('../utils/log-shim') -const openUrl = require('../utils/open-url.js') -const otplease = require('../utils/otplease.js') - -const pollForSession = ({ registry, token, opts }) => { - log.info('adduser', 'Polling for validated SSO session') - return npmFetch.json( - '/-/whoami', { ...opts, registry, forceAuth: { token } } - ).then( - ({ username }) => username, - err => { - if (err.code === 'E401') { - return sleep(opts.ssoPollFrequency).then(() => { - return pollForSession({ registry, token, opts }) - }) - } else { - throw err - } - } - ) -} - -function sleep (time) { - return new Promise((resolve) => setTimeout(resolve, time)) -} - -const login = async (npm, { creds, registry, scope }) => { - const opts = { ...npm.flatOptions, creds, registry, scope } - const { ssoType } = opts - - if (!ssoType) { - throw new Error('Missing option: sso-type') - } - - // We're reusing the legacy login endpoint, so we need some dummy - // stuff here to pass validation. They're never used. - const auth = { - username: 'npm_' + ssoType + '_auth_dummy_user', - password: 'placeholder', - email: 'support@npmjs.com', - authType: ssoType, - } - - const { token, sso } = await otplease(npm, opts, - opts => profile.loginCouch(auth.username, auth.password, opts) - ) - - if (!token) { - throw new Error('no SSO token returned') - } - if (!sso) { - throw new Error('no SSO URL returned by services') - } - - await openUrl(npm, sso, 'to complete your login please visit') - - const username = await pollForSession({ registry, token, opts }) - - log.info('adduser', `Authorized user ${username}`) - - const scopeMessage = scope ? ' to scope ' + scope : '' - const message = `Logged in as ${username}${scopeMessage} on ${registry}.` - - return { - message, - newCreds: { token }, - } -} - -module.exports = login diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index 2853269ef3dee..1e92b35f4a662 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -1,14 +1,8 @@ const log = require('../utils/log-shim.js') const replaceInfo = require('../utils/replace-info.js') +const auth = require('../utils/auth.js') + const BaseCommand = require('../base-command.js') -const authTypes = { - legacy: require('../auth/legacy.js'), - web: require('../auth/legacy.js'), - webauthn: require('../auth/legacy.js'), - oauth: require('../auth/oauth.js'), - saml: require('../auth/saml.js'), - sso: require('../auth/sso.js'), -} class AddUser extends BaseCommand { static description = 'Add a registry user account' @@ -22,63 +16,38 @@ class AddUser extends BaseCommand { static ignoreImplicitWorkspace = true async exec (args) { - const { scope } = this.npm.flatOptions - const registry = this.getRegistry(this.npm.flatOptions) - const auth = this.getAuthType(this.npm.flatOptions) - const creds = this.npm.config.getCredentialsByURI(registry) - - log.disableProgress() - - log.warn('adduser', - '`adduser` will be split into `login` and `register` in a future version.' - + ' `adduser` will become an alias of `register`.' - + ' `login` (currently an alias) will become its own command.') - log.notice('', `Log in on ${replaceInfo(registry)}`) - - const { message, newCreds } = await auth(this.npm, { - ...this.npm.flatOptions, - creds, - registry, - scope, - }) - - await this.updateConfig({ - newCreds, - registry, - scope, - }) - - this.npm.output(message) - } + const scope = this.npm.config.get('scope') + let registry = this.npm.config.get('registry') - getRegistry ({ scope, registry }) { if (scope) { const scopedRegistry = this.npm.config.get(`${scope}:registry`) const cliRegistry = this.npm.config.get('registry', 'cli') if (scopedRegistry && !cliRegistry) { - return scopedRegistry + registry = scopedRegistry } } - return registry - } - getAuthType ({ authType }) { - const type = authTypes[authType] + const creds = this.npm.config.getCredentialsByURI(registry) - if (!type) { - throw new Error('no such auth module') - } + log.disableProgress() + log.notice('', `Log in on ${replaceInfo(registry)}`) - return type - } + const { message, newCreds } = await auth.adduser(this.npm, { + ...this.npm.flatOptions, + creds, + registry, + }) - async updateConfig ({ newCreds, registry, scope }) { this.npm.config.delete('_token', 'user') // prevent legacy pollution this.npm.config.setCredentialsByURI(registry, newCreds) + if (scope) { this.npm.config.set(scope + ':registry', registry, 'user') } + await this.npm.config.save('user') + + this.npm.output(message) } } module.exports = AddUser diff --git a/lib/commands/login.js b/lib/commands/login.js new file mode 100644 index 0000000000000..7f6898d00ba93 --- /dev/null +++ b/lib/commands/login.js @@ -0,0 +1,53 @@ +const log = require('../utils/log-shim.js') +const replaceInfo = require('../utils/replace-info.js') +const auth = require('../utils/auth.js') + +const BaseCommand = require('../base-command.js') + +class Login extends BaseCommand { + static description = 'Login to a registry user account' + static name = 'login' + static params = [ + 'registry', + 'scope', + 'auth-type', + ] + + static ignoreImplicitWorkspace = true + + async exec (args) { + const scope = this.npm.config.get('scope') + let registry = this.npm.config.get('registry') + + if (scope) { + const scopedRegistry = this.npm.config.get(`${scope}:registry`) + const cliRegistry = this.npm.config.get('registry', 'cli') + if (scopedRegistry && !cliRegistry) { + registry = scopedRegistry + } + } + + const creds = this.npm.config.getCredentialsByURI(registry) + + log.disableProgress() + log.notice('', `Log in on ${replaceInfo(registry)}`) + + const { message, newCreds } = await auth.login(this.npm, { + ...this.npm.flatOptions, + creds, + registry, + }) + + this.npm.config.delete('_token', 'user') // prevent legacy pollution + this.npm.config.setCredentialsByURI(registry, newCreds) + + if (scope) { + this.npm.config.set(scope + ':registry', registry, 'user') + } + + await this.npm.config.save('user') + + this.npm.output(message) + } +} +module.exports = Login diff --git a/lib/utils/auth.js b/lib/utils/auth.js new file mode 100644 index 0000000000000..8b9125a1c3ef0 --- /dev/null +++ b/lib/utils/auth.js @@ -0,0 +1,78 @@ +const profile = require('npm-profile') +const log = require('../utils/log-shim') +const openUrlPrompt = require('../utils/open-url-prompt.js') +const read = require('../utils/read-user-info.js') +const otplease = require('../utils/otplease.js') + +const adduser = async (npm, { creds, ...opts }) => { + const authType = npm.config.get('auth-type') + let res + if (authType === 'web') { + res = await profile.adduserWeb((url, emitter) => { + openUrlPrompt( + npm, + url, + 'Create your account at', + 'Press ENTER to open in the browser...', + emitter + ) + }, opts) + } else { + const username = await read.username('Username:', creds.username) + const password = await read.password('Password:', creds.password) + const email = await read.email('Email: (this IS public) ', creds.email) + // npm registry quirk: If you "add" an existing user with their current + // password, it's effectively a login, and if that account has otp you'll + // be prompted for it. + res = await otplease(npm, opts, (reqOpts) => + profile.adduserCouch(username, email, password, opts) + ) + } + + // We don't know the username if it was a web login, all we can reliably log is scope and registry + const message = `Logged in${opts.scope ? ` to scope ${opts.scope}` : ''} on ${opts.registry}.` + + log.info('adduser', message) + + return { + message, + newCreds: { token: res.token }, + } +} + +const login = async (npm, { creds, ...opts }) => { + const authType = npm.config.get('auth-type') + let res + if (authType === 'web') { + res = await profile.loginWeb((url, emitter) => { + openUrlPrompt( + npm, + url, + 'Login at', + 'Press ENTER to open in the browser...', + emitter + ) + }, opts) + } else { + const username = await read.username('Username:', creds.username) + const password = await read.password('Password:', creds.password) + res = await otplease(npm, opts, (reqOpts) => + profile.loginCouch(username, password, reqOpts) + ) + } + + // We don't know the username if it was a web login, all we can reliably log is scope and registry + const message = `Logged in${opts.scope ? ` to scope ${opts.scope}` : ''} on ${opts.registry}.` + + log.info('login', message) + + return { + message, + newCreds: { token: res.token }, + } +} + +module.exports = { + adduser, + login, +} diff --git a/lib/utils/cmd-list.js b/lib/utils/cmd-list.js index c712ece0de2ac..6fffde32a5ee2 100644 --- a/lib/utils/cmd-list.js +++ b/lib/utils/cmd-list.js @@ -4,7 +4,6 @@ const abbrev = require('abbrev') const aliases = { // aliases - login: 'adduser', author: 'owner', home: 'docs', issues: 'bugs', diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index a76484b5228f5..5f10bd5e38898 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -3,7 +3,6 @@ module.exports = definitions const Definition = require('./definition.js') -const log = require('../log-shim') const { version: npmVersion } = require('../../../package.json') const ciDetect = require('@npmcli/ci-detect') const ciName = ciDetect() @@ -239,24 +238,11 @@ define('audit-level', { define('auth-type', { default: 'legacy', - type: ['legacy', 'web', 'sso', 'saml', 'oauth', 'webauthn'], - // deprecation in description rather than field, because not every value - // is deprecated + type: ['legacy', 'web'], description: ` - NOTE: auth-type values "sso", "saml", "oauth", and "webauthn" will be - removed in a future version. - What authentication strategy to use with \`login\`. `, - flatten (key, obj, flatOptions) { - flatOptions.authType = obj[key] - if (obj[key] === 'sso') { - // no need to deprecate saml/oauth here, as sso-type will be set by these in - // lib/auth/ and is deprecated already - log.warn('config', - '--auth-type=sso is will be removed in a future version.') - } - }, + flatten, }) define('before', { @@ -2004,33 +1990,6 @@ define('sign-git-tag', { flatten, }) -define('sso-poll-frequency', { - default: 500, - type: Number, - deprecated: ` - The --auth-type method of SSO/SAML/OAuth will be removed in a future - version of npm in favor of web-based login. - `, - description: ` - When used with SSO-enabled \`auth-type\`s, configures how regularly the - registry should be polled while the user is completing authentication. - `, - flatten, -}) - -define('sso-type', { - default: 'oauth', - type: [null, 'oauth', 'saml'], - deprecated: ` - The --auth-type method of SSO/SAML/OAuth will be removed in a future - version of npm in favor of web-based login. - `, - description: ` - If \`--auth-type=sso\`, the type of SSO type to use. - `, - flatten, -}) - define('strict-peer-deps', { default: false, type: Boolean, diff --git a/lib/utils/otplease.js b/lib/utils/otplease.js index e40ef57730c30..b4aa167469255 100644 --- a/lib/utils/otplease.js +++ b/lib/utils/otplease.js @@ -35,10 +35,10 @@ async function otplease (npm, opts, fn) { } function isWebOTP (err) { - if (!err.code === 'EOTP' || !err.body) { - return false + if (err.code === 'EOTP' && err.body) { + return err.body.authUrl && err.body.doneUrl } - return err.body.authUrl && err.body.doneUrl + return false } function isClassicOTP (err) { diff --git a/tap-snapshots/test/lib/commands/adduser.js.test.cjs b/tap-snapshots/test/lib/commands/adduser.js.test.cjs deleted file mode 100644 index ba27a6a781ab0..0000000000000 --- a/tap-snapshots/test/lib/commands/adduser.js.test.cjs +++ /dev/null @@ -1,17 +0,0 @@ -/* IMPORTANT - * This snapshot file is auto-generated, but designed for humans. - * It should be checked into source control and tracked carefully. - * Re-generate by setting TAP_SNAPSHOT=1 and running tests. - * Make sure to inspect the output below. Do not ignore changes! - */ -'use strict' -exports[`test/lib/commands/adduser.js TAP auth-type sso warning > warning 1`] = ` -Object { - "warn": Array [ - Array [ - "config", - "--auth-type=sso is will be removed in a future version.", - ], - ], -} -` diff --git a/tap-snapshots/test/lib/commands/completion.js.test.cjs b/tap-snapshots/test/lib/commands/completion.js.test.cjs index 85a883bd58b26..3e7125bc33ba9 100644 --- a/tap-snapshots/test/lib/commands/completion.js.test.cjs +++ b/tap-snapshots/test/lib/commands/completion.js.test.cjs @@ -109,7 +109,6 @@ Array [ version view whoami - login author home issues diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index 6ed14ae5b2546..043dccd9ceab4 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -140,8 +140,6 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "shrinkwrap": true, "sign-git-commit": false, "sign-git-tag": false, - "sso-poll-frequency": 500, - "sso-type": "oauth", "strict-peer-deps": false, "strict-ssl": true, "tag": "latest", @@ -297,8 +295,6 @@ shell = "{SHELL}" shrinkwrap = true sign-git-commit = false sign-git-tag = false -sso-poll-frequency = 500 -sso-type = "oauth" strict-peer-deps = false strict-ssl = true tag = "latest" diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index 038121be95435..e3d3d02fc4395 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -30,10 +30,9 @@ Usage: npm adduser Options: -[--registry ] [--scope <@scope>] -[--auth-type ] +[--registry ] [--scope <@scope>] [--auth-type ] -aliases: login, add-user +alias: add-user Run "npm help adduser" for more info ` @@ -479,18 +478,15 @@ Run "npm help ll" for more info ` exports[`test/lib/load-all-commands.js TAP load each command login > must match snapshot 1`] = ` -Add a registry user account +Login to a registry user account Usage: -npm adduser +npm login Options: -[--registry ] [--scope <@scope>] -[--auth-type ] - -aliases: login, add-user +[--registry ] [--scope <@scope>] [--auth-type ] -Run "npm help adduser" for more info +Run "npm help login" for more info ` exports[`test/lib/load-all-commands.js TAP load each command logout > must match snapshot 1`] = ` diff --git a/tap-snapshots/test/lib/npm.js.test.cjs b/tap-snapshots/test/lib/npm.js.test.cjs index 1c3dbe39bd4ae..35a5ee4da3e64 100644 --- a/tap-snapshots/test/lib/npm.js.test.cjs +++ b/tap-snapshots/test/lib/npm.js.test.cjs @@ -187,10 +187,9 @@ All commands: npm adduser Options: - [--registry ] [--scope <@scope>] - [--auth-type ] + [--registry ] [--scope <@scope>] [--auth-type ] - aliases: login, add-user + alias: add-user Run "npm help adduser" for more info @@ -568,18 +567,15 @@ All commands: Run "npm help ll" for more info - login Add a registry user account + login Login to a registry user account Usage: - npm adduser + npm login Options: - [--registry ] [--scope <@scope>] - [--auth-type ] + [--registry ] [--scope <@scope>] [--auth-type ] - aliases: login, add-user - - Run "npm help adduser" for more info + Run "npm help login" for more info logout Log out of the registry diff --git a/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs b/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs index e76f890140b8b..574b0855bc456 100644 --- a/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs +++ b/tap-snapshots/test/lib/utils/cmd-list.js.test.cjs @@ -381,7 +381,6 @@ Object { "la": "ll", "list": "ls", "ln": "link", - "login": "adduser", "ogr": "org", "r": "uninstall", "rb": "rebuild", diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 6eac545b2ce2d..8eff450e7a926 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -135,8 +135,6 @@ Array [ "shrinkwrap", "sign-git-commit", "sign-git-tag", - "sso-poll-frequency", - "sso-type", "strict-peer-deps", "strict-ssl", "tag", @@ -254,10 +252,7 @@ exports[`test/lib/utils/config/definitions.js TAP > config description for auth- #### \`auth-type\` * Default: "legacy" -* Type: "legacy", "web", "sso", "saml", "oauth", or "webauthn" - -NOTE: auth-type values "sso", "saml", "oauth", and "webauthn" will be -removed in a future version. +* Type: "legacy" or "web" What authentication strategy to use with \`login\`. ` @@ -1697,29 +1692,6 @@ Note that git requires you to have set up GPG keys in your git configs for this to work properly. ` -exports[`test/lib/utils/config/definitions.js TAP > config description for sso-poll-frequency 1`] = ` -#### \`sso-poll-frequency\` - -* Default: 500 -* Type: Number -* DEPRECATED: The --auth-type method of SSO/SAML/OAuth will be removed in a - future version of npm in favor of web-based login. - -When used with SSO-enabled \`auth-type\`s, configures how regularly the -registry should be polled while the user is completing authentication. -` - -exports[`test/lib/utils/config/definitions.js TAP > config description for sso-type 1`] = ` -#### \`sso-type\` - -* Default: "oauth" -* Type: null, "oauth", or "saml" -* DEPRECATED: The --auth-type method of SSO/SAML/OAuth will be removed in a - future version of npm in favor of web-based login. - -If \`--auth-type=sso\`, the type of SSO type to use. -` - exports[`test/lib/utils/config/definitions.js TAP > config description for strict-peer-deps 1`] = ` #### \`strict-peer-deps\` diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index 46910d1282e03..633b2da8e0472 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -91,10 +91,7 @@ exit code. #### \`auth-type\` * Default: "legacy" -* Type: "legacy", "web", "sso", "saml", "oauth", or "webauthn" - -NOTE: auth-type values "sso", "saml", "oauth", and "webauthn" will be -removed in a future version. +* Type: "legacy" or "web" What authentication strategy to use with \`login\`. @@ -1957,31 +1954,6 @@ Alias for --package-lock -#### \`sso-poll-frequency\` - -* Default: 500 -* Type: Number -* DEPRECATED: The --auth-type method of SSO/SAML/OAuth will be removed in a - future version of npm in favor of web-based login. - -When used with SSO-enabled \`auth-type\`s, configures how regularly the -registry should be polled while the user is completing authentication. - - - - -#### \`sso-type\` - -* Default: "oauth" -* Type: null, "oauth", or "saml" -* DEPRECATED: The --auth-type method of SSO/SAML/OAuth will be removed in a - future version of npm in favor of web-based login. - -If \`--auth-type=sso\`, the type of SSO type to use. - - - - #### \`tmp\` * Default: The value returned by the Node.js \`os.tmpdir()\` method diff --git a/test/fixtures/mock-registry.js b/test/fixtures/mock-registry.js index 65d4759627aa6..d978929b6b0d8 100644 --- a/test/fixtures/mock-registry.js +++ b/test/fixtures/mock-registry.js @@ -111,17 +111,12 @@ class MockRegistry { } } - couchlogin ({ username, password, email, otp, token = 'npm_default-test-token' }) { - this.nock = this.nock - .post('/-/v1/login').reply(401, { error: 'You must be logged in to publish packages.' }) - if (otp) { - // TODO otp failure results in a 401 with - // {"ok":false,"error":"failed to authenticate: Could not authenticate ${username}: bad otp"} - } + couchadduser ({ username, email, password, token = 'npm_default-test-token' }) { this.nock = this.nock.put(`/-/user/org.couchdb.user:${username}`, body => { this.#tap.match(body, { _id: `org.couchdb.user:${username}`, name: username, + email, // Sole difference from couchlogin password, type: 'user', roles: [], @@ -131,13 +126,59 @@ class MockRegistry { } return true }).reply(201, { - ok: true, id: 'org.couchdb.user:undefined', rev: '_we_dont_use_revs_any_more', token, }) } + couchlogin ({ username, password, token = 'npm_default-test-token' }) { + this.nock = this.nock.put(`/-/user/org.couchdb.user:${username}`, body => { + this.#tap.match(body, { + _id: `org.couchdb.user:${username}`, + name: username, + password, + type: 'user', + roles: [], + }) + if (!body.date) { + return false + } + return true + }).reply(201, { + id: 'org.couchdb.user:undefined', + rev: '_we_dont_use_revs_any_more', + token, + }) + } + + webadduser ({ username, password, token = 'npm_default-test-token' }) { + const doneUrl = new URL('/npm-cli-test/done', this.#registry).href + const loginUrl = new URL('/npm-cli-test/login', this.#registry).href + this.nock = this.nock + .post('/-/v1/login', body => { + this.#tap.ok(body.create) // Sole difference from weblogin + this.#tap.ok(body.hostname) + return true + }) + .reply(200, { doneUrl, loginUrl }) + .get('/npm-cli-test/done') + .reply(200, { token }) + } + + weblogin ({ token = 'npm_default-test-token' }) { + const doneUrl = new URL('/npm-cli-test/done', this.#registry).href + const loginUrl = new URL('/npm-cli-test/login', this.#registry).href + this.nock = this.nock + .post('/-/v1/login', body => { + this.#tap.ok(body.hostname) + return true + }) + .reply(200, { doneUrl, loginUrl }) + .get('/npm-cli-test/done') + .reply(200, { token }) + } + // team can be a team or a username getPackages ({ team, packages = {}, times = 1 }) { if (team.startsWith('@')) { diff --git a/test/lib/auth/legacy.js b/test/lib/auth/legacy.js deleted file mode 100644 index 39d977d436b5e..0000000000000 --- a/test/lib/auth/legacy.js +++ /dev/null @@ -1,429 +0,0 @@ -const t = require('tap') - -let log = '' - -const token = '24528a24f240' -const profile = {} -const read = {} -const legacy = t.mock('../../../lib/auth/legacy.js', { - 'proc-log': { - info: (...msgs) => { - log += msgs.join(' ') - }, - }, - 'npm-profile': profile, - '../../../lib/utils/open-url-prompt.js': (_npm, url) => { - if (!url) { - throw Object.assign(new Error('failed open url'), { code: 'ERROR' }) - } - }, - '../../../lib/utils/read-user-info.js': read, -}) - -const npm = { - config: { - get: () => null, - }, -} - -t.test('login using username/password with token result', async (t) => { - profile.login = () => { - return { token } - } - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user u', - 'should have correct message result' - ) - - t.same( - newCreds, - { token }, - 'should return expected obj from profile.login' - ) - - log = '' - delete profile.login -}) - -t.test('login using username/password with user info result', async (t) => { - profile.login = () => { - return null - } - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.same( - newCreds, - { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - 'should return used credentials' - ) - - log = '' - delete profile.login -}) - -t.test('login otp requested', async (t) => { - t.plan(5) - - profile.login = () => Promise.reject(Object.assign( - new Error('needs otp'), - { code: 'EOTP' } - )) - profile.loginCouch = (username, password, { otp }) => { - t.equal(username, 'u', 'should use provided username to loginCouch') - t.equal(password, 'p', 'should use provided password to loginCouch') - t.equal(otp, '1234', 'should use provided otp code to loginCouch') - - return { token } - } - read.otp = () => Promise.resolve('1234') - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.same( - newCreds, - { token }, - 'should return token from loginCouch result' - ) - - log = '' - delete profile.login - delete profile.loginCouch - delete read.otp -}) - -t.test('login missing basic credential info', async (t) => { - profile.login = () => Promise.reject(Object.assign( - new Error('missing info'), - { code: 'ERROR' } - )) - - await t.rejects( - legacy(npm, { - creds: { - username: 'u', - password: 'p', - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { code: 'ERROR' }, - 'should throw server response error' - ) - - log = '' - delete profile.login -}) - -t.test('create new user when user not found', async (t) => { - t.plan(6) - - profile.login = () => Promise.reject(Object.assign( - new Error('User does not exist'), - { code: 'ERROR' } - )) - profile.adduserCouch = (username, email, password) => { - t.equal(username, 'u', 'should use provided username to adduserCouch') - t.equal(email, 'u@npmjs.org', 'should use provided email to adduserCouch') - t.equal(password, 'p', 'should use provided password to adduserCouch') - - return { token } - } - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as u on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user u', - 'should have correct message result' - ) - - t.same( - newCreds, - { token }, - 'should return expected obj from profile.login' - ) - - log = '' - delete profile.adduserCouch - delete profile.login -}) - -t.test('prompts for user info if required', async (t) => { - t.plan(4) - - profile.login = async (opener, prompt, opts) => { - t.equal(opts.creds.alwaysAuth, true, 'should have refs to creds if any') - await opener('https://registry.npmjs.org/-/v1/login') - const creds = await prompt(opts.creds) - return creds - } - read.username = () => Promise.resolve('foo') - read.password = () => Promise.resolve('pass') - read.email = () => Promise.resolve('foo@npmjs.org') - - const { - message, - newCreds, - } = await legacy(npm, { - creds: { - alwaysAuth: true, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as foo on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user foo', - 'should have correct message result' - ) - - t.same( - newCreds, - { - username: 'foo', - password: 'pass', - email: 'foo@npmjs.org', - alwaysAuth: true, - }, - 'should return result from profile.login containing prompt info' - ) - - log = '' - delete profile.login - delete read.username - delete read.password - delete read.email -}) - -t.test('request otp when creating new user', async (t) => { - t.plan(3) - - profile.login = () => Promise.reject(Object.assign( - new Error('User does not exist'), - { code: 'ERROR' } - )) - profile.adduserCouch = () => Promise.reject(Object.assign( - new Error('needs otp'), - { code: 'EOTP' } - )) - profile.loginCouch = (username, password, { otp }) => { - t.equal(username, 'u', 'should use provided username to loginCouch') - t.equal(password, 'p', 'should use provided password to loginCouch') - t.equal(otp, '1234', 'should now use provided otp code to loginCouch') - - return { token } - } - read.otp = () => Promise.resolve('1234') - - await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - log = '' - delete profile.adduserCouch - delete profile.login - delete profile.loginCouch - delete read.otp -}) - -t.test('unknown error during user creation', async (t) => { - profile.login = () => Promise.reject(Object.assign( - new Error('missing info'), - { code: 'ERROR' } - )) - profile.adduserCouch = () => Promise.reject(Object.assign( - new Error('unkown error'), - { code: 'ERROR' } - )) - - await t.rejects( - legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { code: 'ERROR' }, - 'should throw unknown error' - ) - - log = '' - delete profile.adduserCouch - delete profile.login -}) - -t.test('open url error', async (t) => { - profile.login = async (opener, prompt, opts) => { - await opener() - } - - await t.rejects( - legacy(npm, { - creds: { - username: 'u', - password: 'p', - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'failed open url', code: 'ERROR' }, - 'should throw unknown error' - ) - - log = '' - delete profile.login -}) - -t.test('login no credentials provided', async (t) => { - profile.login = () => ({ token }) - - await legacy(npm, { - creds: { - username: undefined, - password: undefined, - email: undefined, - alwaysAuth: undefined, - }, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - log, - 'login Authorized', - 'should have correct message result' - ) - - log = '' - delete profile.login -}) - -t.test('scoped login', async (t) => { - profile.login = () => ({ token }) - - const { message } = await legacy(npm, { - creds: { - username: 'u', - password: 'p', - email: 'u@npmjs.org', - alwaysAuth: false, - }, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - }) - - t.equal( - message, - 'Logged in as u to scope myscope on https://diff-registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'login Authorized user u', - 'should have correct message result' - ) - - log = '' - delete profile.login -}) diff --git a/test/lib/auth/oauth.js b/test/lib/auth/oauth.js deleted file mode 100644 index 0c317fb9a130e..0000000000000 --- a/test/lib/auth/oauth.js +++ /dev/null @@ -1,28 +0,0 @@ -const t = require('tap') - -t.test('oauth login', (t) => { - t.plan(3) - const oauthOpts = { - creds: {}, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - } - - const npm = { - config: { - set: (key, value) => { - t.equal(key, 'sso-type', 'should define sso-type') - t.equal(value, 'oauth', 'should set sso-type to oauth') - }, - }, - } - const oauth = t.mock('../../../lib/auth/oauth.js', { - '../../../lib/auth/sso.js': (npm, opts) => { - t.equal(opts, oauthOpts, 'should forward opts') - }, - }) - - oauth(npm, oauthOpts) - - t.end() -}) diff --git a/test/lib/auth/saml.js b/test/lib/auth/saml.js deleted file mode 100644 index 1558e0db8eb29..0000000000000 --- a/test/lib/auth/saml.js +++ /dev/null @@ -1,28 +0,0 @@ -const t = require('tap') - -t.test('saml login', (t) => { - t.plan(3) - const samlOpts = { - creds: {}, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - } - - const npm = { - config: { - set: (key, value) => { - t.equal(key, 'sso-type', 'should define sso-type') - t.equal(value, 'saml', 'should set sso-type to saml') - }, - }, - } - const saml = t.mock('../../../lib/auth/saml.js', { - '../../../lib/auth/sso.js': (npm, opts) => { - t.equal(opts, samlOpts, 'should forward opts') - }, - }) - - saml(npm, samlOpts) - - t.end() -}) diff --git a/test/lib/auth/sso.js b/test/lib/auth/sso.js deleted file mode 100644 index 8d70077ad205f..0000000000000 --- a/test/lib/auth/sso.js +++ /dev/null @@ -1,236 +0,0 @@ -const t = require('tap') - -let log = '' - -const _flatOptions = { - ssoType: 'oauth', -} -const token = '24528a24f240' -const SSO_URL = 'https://registry.npmjs.org/{SSO_URL}' -const profile = {} -const npmFetch = {} -const sso = t.mock('../../../lib/auth/sso.js', { - 'proc-log': { - info: (...msgs) => { - log += msgs.join(' ') + '\n' - }, - }, - 'npm-profile': profile, - 'npm-registry-fetch': npmFetch, - '../../../lib/utils/open-url.js': async (npm, url, msg) => { - if (!url) { - throw Object.assign( - new Error('failed open url'), - { code: 'ERROR' } - ) - } - }, -}) - -const npm = { - flatOptions: _flatOptions, -} - -t.test('empty login', async (t) => { - _flatOptions.ssoType = false - - await t.rejects( - sso(npm, {}), - { message: 'Missing option: sso-type' }, - 'should throw if no sso-type defined in flatOptions' - ) - - _flatOptions.ssoType = 'oauth' - log = '' -}) - -t.test('simple login', async (t) => { - t.plan(6) - - profile.loginCouch = (username, password, opts) => { - t.equal(username, 'npm_oauth_auth_dummy_user', 'should use dummy user') - t.equal(password, 'placeholder', 'should use dummy password') - t.same( - opts, - { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - ssoType: 'oauth', - }, - 'should use dummy password' - ) - - return { token, sso: SSO_URL } - } - npmFetch.json = () => Promise.resolve({ username: 'foo' }) - - const { - message, - newCreds, - } = await sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - t.equal( - message, - 'Logged in as foo on https://registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'adduser Polling for validated SSO session\nadduser Authorized user foo\n', - 'should have correct logged info msg' - ) - - t.same( - newCreds, - { token }, - 'should return expected resulting credentials' - ) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) - -t.test('polling retry', async (t) => { - t.plan(3) - - profile.loginCouch = () => ({ token, sso: SSO_URL }) - npmFetch.json = () => { - // assert expected values during retry - npmFetch.json = (url, { registry, forceAuth: { token: expected } }) => { - t.equal( - url, - '/-/whoami', - 'should reach for expected endpoint' - ) - - t.equal( - registry, - 'https://registry.npmjs.org/', - 'should use expected registry value' - ) - - t.equal( - expected, - token, - 'should use expected token retrieved from initial loginCouch' - ) - - return Promise.resolve({ username: 'foo' }) - } - - // initial fetch returns retry code - return Promise.reject(Object.assign( - new Error('nothing yet'), - { code: 'E401' } - )) - } - - await sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) - -t.test('polling error', async (t) => { - profile.loginCouch = () => ({ token, sso: SSO_URL }) - npmFetch.json = () => Promise.reject(Object.assign( - new Error('unknown error'), - { code: 'ERROR' } - )) - - await t.rejects( - sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'unknown error', code: 'ERROR' }, - 'should throw unknown error' - ) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) - -t.test('no token retrieved from loginCouch', async (t) => { - profile.loginCouch = () => ({}) - - await t.rejects( - sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'no SSO token returned' }, - 'should throw no SSO token returned error' - ) - - log = '' - delete profile.loginCouch -}) - -t.test('no sso url retrieved from loginCouch', async (t) => { - profile.loginCouch = () => Promise.resolve({ token }) - - await t.rejects( - sso(npm, { - creds: {}, - registry: 'https://registry.npmjs.org/', - scope: '', - }), - { message: 'no SSO URL returned by services' }, - 'should throw no SSO url returned error' - ) - - log = '' - delete profile.loginCouch -}) - -t.test('scoped login', async (t) => { - profile.loginCouch = () => ({ token, sso: SSO_URL }) - npmFetch.json = () => Promise.resolve({ username: 'foo' }) - - const { - message, - newCreds, - } = await sso(npm, { - creds: {}, - registry: 'https://diff-registry.npmjs.org/', - scope: 'myscope', - }) - - t.equal( - message, - 'Logged in as foo to scope myscope on https://diff-registry.npmjs.org/.', - 'should have correct message result' - ) - - t.equal( - log, - 'adduser Polling for validated SSO session\nadduser Authorized user foo\n', - 'should have correct logged info msg' - ) - - t.same( - newCreds, - { token }, - 'should return expected resulting credentials' - ) - - log = '' - delete profile.loginCouch - delete npmFetch.json -}) diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index 94e58a6d3cb0e..4ff65e57f30dd 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -1,7 +1,7 @@ const t = require('tap') -const path = require('path') const fs = require('fs') -const os = require('os') +const path = require('path') +const ini = require('ini') const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const mockGlobals = require('../../fixtures/mock-globals.js') @@ -14,170 +14,169 @@ t.test('usage', async t => { t.match(adduser.usage, 'adduser', 'usage has command name in it') }) -t.test('simple login', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm, home } = await loadMockNpm(t, { - homeDir: { - // These all get cleaned up by config.setCredentialsByURI - '.npmrc': [ - '_token=user', - '_password=user', - 'username=user', - '_auth=user', - '_authtoken=user', - '-authtoken=user', - '_authToken=user', - '//registry.npmjs.org/:_authToken=user', - '//registry.npmjs.org/:always-auth=user', - '//registry.npmjs.org/:email=test-email-old@npmjs.org', - ].join('\n'), - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), +t.test('legacy', async t => { + t.test('simple adduser', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + homeDir: { + // These all get cleaned up by config.setCredentialsByURI + '.npmrc': [ + '_token=user', + '_password=user', + 'username=user', + '_auth=user', + '_authtoken=user', + '-authtoken=user', + '_authToken=user', + '//registry.npmjs.org/:_authToken=user', + '//registry.npmjs.org/:always-auth=user', + '//registry.npmjs.org/:email=test-email-old@npmjs.org', + ].join('\n'), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await npm.exec('adduser', []) + t.same(npm.config.get('email'), 'test-email-old@npmjs.org') + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + email: 'test-email-old@npmjs.org', + }, 'should only have token and un-nerfed old email') }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', - }) - await npm.exec('adduser', []) - t.same(npm.config.get('email'), 'test-email-old@npmjs.org') - t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') - const rc = fs.readFileSync(path.join(home, '.npmrc'), 'utf8') - t.same( - rc.trim().split(os.EOL), [ - '//registry.npmjs.org/:_authToken=npm_test-token', - 'email=test-email-old@npmjs.org', - ], 'should only have token and un-nerfed old email' - ) -}) -t.test('bad auth type', async t => { - const { npm } = await loadMockNpm(t, { - config: { - 'auth-type': 'foo', - }, + t.test('scoped adduser', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { + scope: '@myscope', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await npm.exec('adduser', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@myscope:registry'), 'https://registry.npmjs.org/') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + '@myscope:registry': 'https://registry.npmjs.org/', + }, 'should only have token and scope:registry') }) - await t.rejects(npm.exec('adduser', []), { - message: 'no such auth module', - }) -}) -t.test('auth-type sso warning', async t => { - const { logs } = await loadMockNpm(t, { - config: { - 'auth-type': 'sso', - }, + t.test('scoped adduser with valid scoped registry config', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + homeDir: { + '.npmrc': '@myscope:registry=https://diff-registry.npmjs.org', + }, + config: { + scope: '@myscope', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: 'https://diff-registry.npmjs.org', + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await npm.exec('adduser', []) + t.same(npm.config.get('//diff-registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@myscope:registry'), 'https://diff-registry.npmjs.org') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '@myscope:registry': 'https://diff-registry.npmjs.org', + '//diff-registry.npmjs.org/:_authToken': 'npm_test-token', + }, 'should only have token and scope:registry') }) - t.matchSnapshot({ warn: logs.warn }, 'warning') -}) -t.test('scoped login', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm, home } = await loadMockNpm(t, { - config: { - scope: '@myscope', - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), + t.test('save config failure', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + stdin.write('test-email@npmjs.org\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm } = await loadMockNpm(t, { + homeDir: { + '.npmrc': {}, + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchadduser({ + username: 'test-user', + password: 'test-password', + email: 'test-email@npmjs.org', + token: 'npm_test-token', + }) + await t.rejects(npm.exec('adduser', [])) }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', - }) - await npm.exec('adduser', []) - t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') - t.same(npm.config.get('@myscope:registry'), 'https://registry.npmjs.org/') - const rc = fs.readFileSync(path.join(home, '.npmrc'), 'utf8') - t.same( - rc.trim().split(os.EOL), [ - '//registry.npmjs.org/:_authToken=npm_test-token', - '@myscope:registry=https://registry.npmjs.org/', - ], 'should only have token and scope:registry') -}) - -t.test('scoped login with valid scoped registry config', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm, home } = await loadMockNpm(t, { - homeDir: { - '.npmrc': '@myscope:registry=https://diff-registry.npmjs.org', - }, - config: { - scope: '@myscope', - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: 'https://diff-registry.npmjs.org', - }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', - }) - await npm.exec('adduser', []) - t.same(npm.config.get('//diff-registry.npmjs.org/:_authToken'), 'npm_test-token') - t.same(npm.config.get('@myscope:registry'), 'https://diff-registry.npmjs.org') - const rc = fs.readFileSync(path.join(home, '.npmrc'), 'utf8') - t.same(rc.trim().split(os.EOL), - [ - '@myscope:registry=https://diff-registry.npmjs.org', - '//diff-registry.npmjs.org/:_authToken=npm_test-token', - ], 'should only have token and scope:registry') + t.end() }) -t.test('save config failure', async t => { - const stdin = new stream.PassThrough() - stdin.write('test-user\n') - stdin.write('test-password\n') - stdin.write('test-email@npmjs.org\n') - mockGlobals(t, { - 'process.stdin': stdin, - 'process.stdout': new stream.PassThrough(), // to quiet readline - }, { replace: true }) - const { npm } = await loadMockNpm(t, { - homeDir: { - '.npmrc': {}, - }, - }) - const registry = new MockRegistry({ - tap: t, - registry: npm.config.get('registry'), - }) - registry.couchlogin({ - username: 'test-user', - password: 'test-password', - email: 'test-email@npmjs.org', - token: 'npm_test-token', +t.test('web', t => { + t.test('basic adduser', async t => { + const { npm, home } = await loadMockNpm(t, { + config: { 'auth-type': 'web' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.webadduser({ token: 'npm_test-token' }) + await npm.exec('adduser', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + }) }) - await t.rejects(npm.exec('adduser', [])) + t.end() }) diff --git a/test/lib/commands/login.js b/test/lib/commands/login.js new file mode 100644 index 0000000000000..8d27421313406 --- /dev/null +++ b/test/lib/commands/login.js @@ -0,0 +1,151 @@ +const t = require('tap') +const fs = require('fs') +const path = require('path') +const ini = require('ini') + +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') +const mockGlobals = require('../../fixtures/mock-globals.js') +const MockRegistry = require('../../fixtures/mock-registry.js') +const stream = require('stream') + +t.test('usage', async t => { + const { npm } = await loadMockNpm(t) + const login = await npm.cmd('login') + t.match(login.usage, 'login', 'usage has command name in it') +}) + +t.test('legacy', t => { + t.test('basic login', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { 'auth-type': 'legacy' }, + homeDir: { + // These all get cleaned up by config.setCredentialsByURI + '.npmrc': [ + '_token=user', + '_password=user', + 'username=user', + '_auth=user', + '_authtoken=user', + '-authtoken=user', + '_authToken=user', + '//registry.npmjs.org/:_authToken=user', + '//registry.npmjs.org/:always-auth=user', + '//registry.npmjs.org/:email=test-email-old@npmjs.org', + ].join('\n'), + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchlogin({ + username: 'test-user', + password: 'test-password', + token: 'npm_test-token', + }) + await npm.exec('login', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + email: 'test-email-old@npmjs.org', + }, 'should only have token and un-nerfed old email') + }) + + t.test('scoped login default registry', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { + 'auth-type': 'legacy', + scope: '@npmcli', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.couchlogin({ + username: 'test-user', + password: 'test-password', + token: 'npm_test-token', + }) + await npm.exec('login', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@npmcli:registry'), 'https://registry.npmjs.org/') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + '@npmcli:registry': 'https://registry.npmjs.org/', + }, 'should only have token and scope:registry') + }) + + t.test('scoped login scoped registry', async t => { + const stdin = new stream.PassThrough() + stdin.write('test-user\n') + stdin.write('test-password\n') + mockGlobals(t, { + 'process.stdin': stdin, + 'process.stdout': new stream.PassThrough(), // to quiet readline + }, { replace: true }) + const { npm, home } = await loadMockNpm(t, { + config: { + 'auth-type': 'legacy', + scope: '@npmcli', + }, + homeDir: { + '.npmrc': '@npmcli:registry=https://diff-registry.npmjs.org', + }, + }) + const registry = new MockRegistry({ + tap: t, + registry: 'https://diff-registry.npmjs.org', + }) + registry.couchlogin({ + username: 'test-user', + password: 'test-password', + token: 'npm_test-token', + }) + await npm.exec('login', []) + t.same(npm.config.get('//diff-registry.npmjs.org/:_authToken'), 'npm_test-token') + t.same(npm.config.get('@npmcli:registry'), 'https://diff-registry.npmjs.org') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '@npmcli:registry': 'https://diff-registry.npmjs.org', + '//diff-registry.npmjs.org/:_authToken': 'npm_test-token', + }, 'should only have token and scope:registry') + }) + t.end() +}) + +t.test('web', t => { + t.test('basic login', async t => { + const { npm, home } = await loadMockNpm(t, { + config: { 'auth-type': 'web' }, + }) + const registry = new MockRegistry({ + tap: t, + registry: npm.config.get('registry'), + }) + registry.weblogin({ token: 'npm_test-token' }) + await npm.exec('login', []) + t.same(npm.config.get('//registry.npmjs.org/:_authToken'), 'npm_test-token') + const rc = ini.parse(fs.readFileSync(path.join(home, '.npmrc'), 'utf8')) + t.same(rc, { + '//registry.npmjs.org/:_authToken': 'npm_test-token', + }) + }) + t.end() +})