123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- const inspect = require('util').inspect
- const { URL } = require('url')
- const ansistyles = require('ansistyles')
- const log = require('npmlog')
- const npmProfile = require('npm-profile')
- const qrcodeTerminal = require('qrcode-terminal')
- const Table = require('cli-table3')
- const otplease = require('./utils/otplease.js')
- const pulseTillDone = require('./utils/pulse-till-done.js')
- const readUserInfo = require('./utils/read-user-info.js')
- const qrcode = url =>
- new Promise((resolve) => qrcodeTerminal.generate(url, resolve))
- const knownProfileKeys = [
- 'name',
- 'email',
- 'two-factor auth',
- 'fullname',
- 'homepage',
- 'freenode',
- 'twitter',
- 'github',
- 'created',
- 'updated',
- ]
- const writableProfileKeys = [
- 'email',
- 'password',
- 'fullname',
- 'homepage',
- 'freenode',
- 'twitter',
- 'github',
- ]
- const BaseCommand = require('./base-command.js')
- class Profile extends BaseCommand {
- static get description () {
- return 'Change settings on your registry profile'
- }
- /* istanbul ignore next - see test/lib/load-all-commands.js */
- static get name () {
- return 'profile'
- }
- /* istanbul ignore next - see test/lib/load-all-commands.js */
- static get usage () {
- return [
- 'enable-2fa [auth-only|auth-and-writes]',
- 'disable-2fa',
- 'get [<key>]',
- 'set <key> <value>',
- ]
- }
- /* istanbul ignore next - see test/lib/load-all-commands.js */
- static get params () {
- return [
- 'registry',
- 'json',
- 'parseable',
- 'otp',
- ]
- }
- async completion (opts) {
- var argv = opts.conf.argv.remain
- if (!argv[2])
- return ['enable-2fa', 'disable-2fa', 'get', 'set']
- switch (argv[2]) {
- case 'enable-2fa':
- case 'enable-tfa':
- return ['auth-and-writes', 'auth-only']
- case 'disable-2fa':
- case 'disable-tfa':
- case 'get':
- case 'set':
- return []
- default:
- throw new Error(argv[2] + ' not recognized')
- }
- }
- exec (args, cb) {
- this.profile(args).then(() => cb()).catch(cb)
- }
- async profile (args) {
- if (args.length === 0)
- throw new Error(this.usage)
- log.gauge.show('profile')
- const [subcmd, ...opts] = args
- switch (subcmd) {
- case 'enable-2fa':
- case 'enable-tfa':
- case 'enable2fa':
- case 'enabletfa':
- return this.enable2fa(opts)
- case 'disable-2fa':
- case 'disable-tfa':
- case 'disable2fa':
- case 'disabletfa':
- return this.disable2fa()
- case 'get':
- return this.get(opts)
- case 'set':
- return this.set(opts)
- default:
- throw new Error('Unknown profile command: ' + subcmd)
- }
- }
- async get (args) {
- const tfa = 'two-factor auth'
- const info = await pulseTillDone.withPromise(
- npmProfile.get(this.npm.flatOptions)
- )
- if (!info.cidr_whitelist)
- delete info.cidr_whitelist
- if (this.npm.config.get('json')) {
- this.npm.output(JSON.stringify(info, null, 2))
- return
- }
- // clean up and format key/values for output
- const cleaned = {}
- for (const key of knownProfileKeys)
- cleaned[key] = info[key] || ''
- const unknownProfileKeys = Object.keys(info).filter((k) => !(k in cleaned))
- for (const key of unknownProfileKeys)
- cleaned[key] = info[key] || ''
- delete cleaned.tfa
- delete cleaned.email_verified
- cleaned.email += info.email_verified ? ' (verified)' : '(unverified)'
- if (info.tfa && !info.tfa.pending)
- cleaned[tfa] = info.tfa.mode
- else
- cleaned[tfa] = 'disabled'
- if (args.length) {
- const values = args // comma or space separated
- .join(',')
- .split(/,/)
- .filter((arg) => arg.trim() !== '')
- .map((arg) => cleaned[arg])
- .join('\t')
- this.npm.output(values)
- } else {
- if (this.npm.config.get('parseable')) {
- for (const key of Object.keys(info)) {
- if (key === 'tfa')
- this.npm.output(`${key}\t${cleaned[tfa]}`)
- else
- this.npm.output(`${key}\t${info[key]}`)
- }
- } else {
- const table = new Table()
- for (const key of Object.keys(cleaned))
- table.push({ [ansistyles.bright(key)]: cleaned[key] })
- this.npm.output(table.toString())
- }
- }
- }
- async set (args) {
- const conf = this.npm.flatOptions
- const prop = (args[0] || '').toLowerCase().trim()
- let value = args.length > 1 ? args.slice(1).join(' ') : null
- const readPasswords = async () => {
- const newpassword = await readUserInfo.password('New password: ')
- const confirmedpassword = await readUserInfo.password(' Again: ')
- if (newpassword !== confirmedpassword) {
- log.warn('profile', 'Passwords do not match, please try again.')
- return readPasswords()
- }
- return newpassword
- }
- if (prop !== 'password' && value === null)
- throw new Error('npm profile set <prop> <value>')
- if (prop === 'password' && value !== null) {
- throw new Error(
- 'npm profile set password\n' +
- 'Do not include your current or new passwords on the command line.')
- }
- if (writableProfileKeys.indexOf(prop) === -1) {
- throw new Error(`"${prop}" is not a property we can set. ` +
- `Valid properties are: ` + writableProfileKeys.join(', '))
- }
- if (prop === 'password') {
- const current = await readUserInfo.password('Current password: ')
- const newpassword = await readPasswords()
- value = { old: current, new: newpassword }
- }
- // FIXME: Work around to not clear everything other than what we're setting
- const user = await pulseTillDone.withPromise(npmProfile.get(conf))
- const newUser = {}
- for (const key of writableProfileKeys)
- newUser[key] = user[key]
- newUser[prop] = value
- const result = await otplease(conf, conf => npmProfile.set(newUser, conf))
- if (this.npm.config.get('json'))
- this.npm.output(JSON.stringify({ [prop]: result[prop] }, null, 2))
- else if (this.npm.config.get('parseable'))
- this.npm.output(prop + '\t' + result[prop])
- else if (result[prop] != null)
- this.npm.output('Set', prop, 'to', result[prop])
- else
- this.npm.output('Set', prop)
- }
- async enable2fa (args) {
- if (args.length > 1)
- throw new Error('npm profile enable-2fa [auth-and-writes|auth-only]')
- const mode = args[0] || 'auth-and-writes'
- if (mode !== 'auth-only' && mode !== 'auth-and-writes') {
- throw new Error(
- `Invalid two-factor authentication mode "${mode}".\n` +
- 'Valid modes are:\n' +
- ' auth-only - Require two-factor authentication only when logging in\n' +
- ' auth-and-writes - Require two-factor authentication when logging in ' +
- 'AND when publishing'
- )
- }
- if (this.npm.config.get('json') || this.npm.config.get('parseable')) {
- throw new Error(
- 'Enabling two-factor authentication is an interactive operation and ' +
- (this.npm.config.get('json') ? 'JSON' : 'parseable') + ' output mode is not available'
- )
- }
- const info = {
- tfa: {
- mode: mode,
- },
- }
- // if they're using legacy auth currently then we have to
- // update them to a bearer token before continuing.
- const creds = this.npm.config.getCredentialsByURI(this.npm.config.get('registry'))
- const auth = {}
- if (creds.token)
- auth.token = creds.token
- else if (creds.username)
- auth.basic = { username: creds.username, password: creds.password }
- else if (creds.auth) {
- const basic = Buffer.from(creds.auth, 'base64').toString().split(':', 2)
- auth.basic = { username: basic[0], password: basic[1] }
- }
- if (!auth.basic && !auth.token) {
- throw new Error(
- 'You need to be logged in to registry ' +
- `${this.npm.config.get('registry')} in order to enable 2fa`
- )
- }
- if (auth.basic) {
- log.info('profile', 'Updating authentication to bearer token')
- const result = await npmProfile.createToken(
- auth.basic.password, false, [], this.npm.flatOptions
- )
- if (!result.token) {
- throw new Error(
- `Your registry ${this.npm.config.get('registry')} does not seem to ` +
- 'support bearer tokens. Bearer tokens are required for ' +
- 'two-factor authentication'
- )
- }
- this.npm.config.setCredentialsByURI(
- this.npm.config.get('registry'),
- { token: result.token }
- )
- await this.npm.config.save('user')
- }
- log.notice('profile', 'Enabling two factor authentication for ' + mode)
- const password = await readUserInfo.password()
- info.tfa.password = password
- log.info('profile', 'Determine if tfa is pending')
- const userInfo = await pulseTillDone.withPromise(
- npmProfile.get(this.npm.flatOptions)
- )
- const conf = { ...this.npm.flatOptions }
- if (userInfo && userInfo.tfa && userInfo.tfa.pending) {
- log.info('profile', 'Resetting two-factor authentication')
- await pulseTillDone.withPromise(
- npmProfile.set({ tfa: { password, mode: 'disable' } }, conf)
- )
- } else if (userInfo && userInfo.tfa) {
- if (!conf.otp) {
- conf.otp = await readUserInfo.otp(
- 'Enter one-time password from your authenticator app: '
- )
- }
- }
- log.info('profile', 'Setting two-factor authentication to ' + mode)
- const challenge = await pulseTillDone.withPromise(
- npmProfile.set(info, conf)
- )
- if (challenge.tfa === null) {
- this.npm.output('Two factor authentication mode changed to: ' + mode)
- return
- }
- const badResponse = typeof challenge.tfa !== 'string'
- || !/^otpauth:[/][/]/.test(challenge.tfa)
- if (badResponse) {
- throw new Error(
- 'Unknown error enabling two-factor authentication. Expected otpauth URL' +
- ', got: ' + inspect(challenge.tfa)
- )
- }
- const otpauth = new URL(challenge.tfa)
- const secret = otpauth.searchParams.get('secret')
- const code = await qrcode(challenge.tfa)
- this.npm.output(
- 'Scan into your authenticator app:\n' + code + '\n Or enter code:', secret
- )
- const interactiveOTP =
- await readUserInfo.otp('And an OTP code from your authenticator: ')
- log.info('profile', 'Finalizing two-factor authentication')
- const result = await npmProfile.set({ tfa: [interactiveOTP] }, conf)
- this.npm.output(
- '2FA successfully enabled. Below are your recovery codes, ' +
- 'please print these out.'
- )
- this.npm.output(
- 'You will need these to recover access to your account ' +
- 'if you lose your authentication device.'
- )
- for (const tfaCode of result.tfa)
- this.npm.output('\t' + tfaCode)
- }
- async disable2fa (args) {
- const conf = { ...this.npm.flatOptions }
- const info = await pulseTillDone.withPromise(npmProfile.get(conf))
- if (!info.tfa || info.tfa.pending) {
- this.npm.output('Two factor authentication not enabled.')
- return
- }
- const password = await readUserInfo.password()
- if (!conf.otp) {
- const msg = 'Enter one-time password from your authenticator app: '
- conf.otp = await readUserInfo.otp(msg)
- }
- log.info('profile', 'disabling tfa')
- await pulseTillDone.withPromise(npmProfile.set({
- tfa: { password: password, mode: 'disable' },
- }, conf))
- if (this.npm.config.get('json'))
- this.npm.output(JSON.stringify({ tfa: false }, null, 2))
- else if (this.npm.config.get('parseable'))
- this.npm.output('tfa\tfalse')
- else
- this.npm.output('Two factor authentication disabled.')
- }
- }
- module.exports = Profile
|