Skip to content

Commit 85027f4

Browse files
ruyadornoisaacs
authored andcommitted
chore: refactor adduser
- Refactored lib/auth modules - Removed some of the extra wrappers from cb to promises - Replaced impl to favor usage of async/await - Added test/lib/auth unit tests PR-URL: #1664 Credit: @ruyadorno Close: #1664 Reviewed-by: @isaacs
1 parent 9e7cc42 commit 85027f4

File tree

10 files changed

+1122
-125
lines changed

10 files changed

+1122
-125
lines changed

lib/adduser.js

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
'use strict'
2+
13
const log = require('npmlog')
24
const npm = require('./npm.js')
5+
const output = require('./utils/output.js')
36
const usageUtil = require('./utils/usage.js')
7+
const authTypes = {
8+
legacy: require('./auth/legacy.js'),
9+
oauth: require('./auth/oauth.js'),
10+
saml: require('./auth/saml.js'),
11+
sso: require('./auth/sso.js')
12+
}
413

514
const usage = usageUtil(
615
'adduser',
@@ -11,8 +20,7 @@ const completion = require('./utils/completion/none.js')
1120

1221
const cmd = (args, cb) => adduser(args).then(() => cb()).catch(cb)
1322

14-
const getRegistry = opts => {
15-
const { scope, registry } = opts
23+
const getRegistry = ({ scope, registry }) => {
1624
if (scope) {
1725
const scopedRegistry = npm.config.get(`${scope}:registry`)
1826
const cliRegistry = npm.config.get('registry', 'cli')
@@ -24,37 +32,51 @@ const getRegistry = opts => {
2432
}
2533

2634
const getAuthType = ({ authType }) => {
27-
try {
28-
return require('./auth/' + authType)
29-
} catch (e) {
35+
const type = authTypes[authType]
36+
37+
if (!type) {
3038
throw new Error('no such auth module')
3139
}
40+
41+
return type
3242
}
3343

34-
const adduser = async args => {
35-
const registry = getRegistry(npm.flatOptions)
44+
const saveConfig = () => new Promise((resolve, reject) => {
45+
npm.config.save('user', er => er ? reject(er) : resolve())
46+
})
47+
48+
const updateConfig = async ({ newCreds, registry, scope }) => {
49+
npm.config.del('_token', 'user') // prevent legacy pollution
50+
51+
if (scope) {
52+
npm.config.set(scope + ':registry', registry, 'user')
53+
}
54+
55+
npm.config.setCredentialsByURI(registry, newCreds)
56+
await saveConfig()
57+
}
58+
59+
const adduser = async (args) => {
3660
const { scope } = npm.flatOptions
61+
const registry = getRegistry(npm.flatOptions)
62+
const auth = getAuthType(npm.flatOptions)
3763
const creds = npm.config.getCredentialsByURI(registry)
3864

3965
log.disableProgress()
4066

41-
const auth = getAuthType(npm.flatOptions)
67+
const { message, newCreds } = await auth({
68+
creds,
69+
registry,
70+
scope
71+
})
4272

43-
// XXX make auth.login() promise-returning so we don't have to wrap here
44-
await new Promise((res, rej) => {
45-
auth.login(creds, registry, scope, function (er, newCreds) {
46-
if (er) {
47-
return rej(er)
48-
}
49-
50-
npm.config.del('_token', 'user') // prevent legacy pollution
51-
if (scope) {
52-
npm.config.set(scope + ':registry', registry, 'user')
53-
}
54-
npm.config.setCredentialsByURI(registry, newCreds)
55-
npm.config.save('user', er => er ? rej(er) : res())
56-
})
73+
await updateConfig({
74+
newCreds,
75+
registry,
76+
scope
5777
})
78+
79+
output(message)
5880
}
5981

6082
module.exports = Object.assign(cmd, { completion, usage })

lib/auth/legacy.js

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,101 @@
11
'use strict'
22

3-
const read = require('../utils/read-user-info.js')
4-
const profile = require('npm-profile')
53
const log = require('npmlog')
6-
const npm = require('../npm.js')
7-
const output = require('../utils/output.js')
8-
const openUrl = require('../utils/open-url')
4+
const profile = require('npm-profile')
95

6+
const openUrl = require('../utils/open-url.js')
7+
const read = require('../utils/read-user-info.js')
8+
9+
// TODO: refactor lib/utils/open-url and its usages
1010
const openerPromise = (url) => new Promise((resolve, reject) => {
1111
openUrl(url, 'to complete your login please visit', (er) => er ? reject(er) : resolve())
1212
})
1313

14-
const loginPrompter = (creds) => {
14+
const loginPrompter = async (creds) => {
1515
const opts = { log: log }
16-
return read.username('Username:', creds.username, opts).then((u) => {
17-
creds.username = u
18-
return read.password('Password:', creds.password)
19-
}).then((p) => {
20-
creds.password = p
21-
return read.email('Email: (this IS public) ', creds.email, opts)
22-
}).then((e) => {
23-
creds.email = e
24-
return creds
25-
})
16+
17+
creds.username = await read.username('Username:', creds.username, opts)
18+
creds.password = await read.password('Password:', creds.password)
19+
creds.email = await read.email('Email: (this IS public) ', creds.email, opts)
20+
21+
return creds
2622
}
2723

28-
module.exports.login = (creds = {}, registry, scope, cb) => {
29-
const opts = {
30-
...npm.flatOptions,
31-
scope,
32-
registry,
33-
creds
24+
const login = async (opts) => {
25+
let res
26+
27+
const requestOTP = async () => {
28+
const otp = await read.otp(
29+
'Enter one-time password from your authenticator app: '
30+
)
31+
32+
return profile.loginCouch(
33+
opts.creds.username,
34+
opts.creds.password,
35+
{ ...opts, otp }
36+
)
3437
}
35-
login(opts).then((newCreds) => cb(null, newCreds)).catch(cb)
36-
}
3738

38-
function login (opts) {
39-
return profile.login(openerPromise, loginPrompter, opts)
40-
.catch((err) => {
41-
if (err.code === 'EOTP') throw err
42-
const u = opts.creds.username
43-
const p = opts.creds.password
44-
const e = opts.creds.email
45-
if (!(u && p && e)) throw err
46-
return profile.adduserCouch(u, e, p, opts)
47-
})
48-
.catch((err) => {
49-
if (err.code !== 'EOTP') throw err
50-
return read.otp(
51-
'Enter one-time password from your authenticator app: '
52-
).then(otp => {
53-
const u = opts.creds.username
54-
const p = opts.creds.password
55-
return profile.loginCouch(u, p, { ...opts, otp })
56-
})
57-
}).then((result) => {
58-
const newCreds = {}
59-
if (result && result.token) {
60-
newCreds.token = result.token
39+
const addNewUser = async () => {
40+
let newUser
41+
42+
try {
43+
newUser = await profile.adduserCouch(
44+
opts.creds.username,
45+
opts.creds.email,
46+
opts.creds.password,
47+
opts
48+
)
49+
} catch (err) {
50+
if (err.code === 'EOTP') {
51+
newUser = await requestOTP()
6152
} else {
62-
newCreds.username = opts.creds.username
63-
newCreds.password = opts.creds.password
64-
newCreds.email = opts.creds.email
65-
newCreds.alwaysAuth = opts.alwaysAuth
53+
throw err
6654
}
55+
}
6756

68-
const usermsg = opts.creds.username ? ' user ' + opts.creds.username : ''
69-
opts.log.info('login', 'Authorized' + usermsg)
70-
const scopeMessage = opts.scope ? ' to scope ' + opts.scope : ''
71-
const userout = opts.creds.username ? ' as ' + opts.creds.username : ''
72-
output('Logged in%s%s on %s.', userout, scopeMessage, opts.registry)
73-
return newCreds
74-
})
57+
return newUser
58+
}
59+
60+
try {
61+
res = await profile.login(openerPromise, loginPrompter, opts)
62+
} catch (err) {
63+
const needsMoreInfo = !(opts &&
64+
opts.creds &&
65+
opts.creds.username &&
66+
opts.creds.password &&
67+
opts.creds.email)
68+
if (err.code === 'EOTP') {
69+
res = await requestOTP()
70+
} else if (needsMoreInfo) {
71+
throw err
72+
} else {
73+
// TODO: maybe this needs to check for err.code === 'E400' instead?
74+
res = await addNewUser()
75+
}
76+
}
77+
78+
const newCreds = {}
79+
if (res && res.token) {
80+
newCreds.token = res.token
81+
} else {
82+
newCreds.username = opts.creds.username
83+
newCreds.password = opts.creds.password
84+
newCreds.email = opts.creds.email
85+
newCreds.alwaysAuth = opts.creds.alwaysAuth
86+
}
87+
88+
const usermsg = opts.creds.username ? ` user ${opts.creds.username}` : ''
89+
const scopeMessage = opts.scope ? ` to scope ${opts.scope}` : ''
90+
const userout = opts.creds.username ? ` as ${opts.creds.username}` : ''
91+
const message = `Logged in${userout}${scopeMessage} on ${opts.registry}.`
92+
93+
log.info('login', `Authorized${usermsg}`)
94+
95+
return {
96+
message,
97+
newCreds
98+
}
7599
}
100+
101+
module.exports = login

lib/auth/oauth.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
var ssoAuth = require('./sso')
2-
var npm = require('../npm')
1+
const sso = require('./sso.js')
2+
const npm = require('../npm.js')
33

4-
module.exports.login = function login () {
4+
const login = (opts) => {
55
npm.config.set('sso-type', 'oauth')
6-
ssoAuth.login.apply(this, arguments)
6+
return sso(opts)
77
}
8+
9+
module.exports = login

lib/auth/saml.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
var ssoAuth = require('./sso')
2-
var npm = require('../npm')
1+
const sso = require('./sso.js')
2+
const npm = require('../npm.js')
33

4-
module.exports.login = function login () {
4+
const login = (opts) => {
55
npm.config.set('sso-type', 'saml')
6-
ssoAuth.login.apply(this, arguments)
6+
return sso(opts)
77
}
8+
9+
module.exports = login

lib/auth/sso.js

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,17 @@
99
// CLI, we can remove this, and fold the lib/auth/legacy.js back into
1010
// lib/adduser.js
1111

12+
const { promisify } = require('util')
13+
1214
const log = require('npmlog')
13-
const npm = require('../npm.js')
15+
const profile = require('npm-profile')
1416
const npmFetch = require('npm-registry-fetch')
15-
const output = require('../utils/output.js')
16-
const { promisify } = require('util')
17+
18+
const npm = require('../npm.js')
1719
const openUrl = promisify(require('../utils/open-url.js'))
1820
const otplease = require('../utils/otplease.js')
19-
const profile = require('npm-profile')
20-
21-
module.exports.login = function login (creds, registry, scope, cb) {
22-
log.warn('deprecated', 'SSO --auth-type is deprecated')
23-
const opts = { ...npm.flatOptions, creds, registry, scope }
24-
const ssoType = opts.ssoType
25-
if (!ssoType) { return cb(new Error('Missing option: sso-type')) }
2621

27-
// We're reusing the legacy login endpoint, so we need some dummy
28-
// stuff here to pass validation. They're never used.
29-
const auth = {
30-
username: 'npm_' + ssoType + '_auth_dummy_user',
31-
password: 'placeholder',
32-
email: 'support@npmjs.com',
33-
authType: ssoType
34-
}
35-
36-
otplease(opts,
37-
opts => profile.loginCouch(auth.username, auth.password, opts)
38-
).then(({ token, sso }) => {
39-
if (!token) { throw new Error('no SSO token returned') }
40-
if (!sso) { throw new Error('no SSO URL returned by services') }
41-
return openUrl(sso, 'to complete your login please visit').then(() => {
42-
return pollForSession(registry, token, opts)
43-
}).then(username => {
44-
log.info('adduser', 'Authorized user %s', username)
45-
var scopeMessage = scope ? ' to scope ' + scope : ''
46-
output('Logged in as %s%s on %s.', username, scopeMessage, registry)
47-
return { token }
48-
})
49-
}).then(res => cb(null, res), cb)
50-
}
51-
52-
function pollForSession (registry, token, opts) {
22+
const pollForSession = ({ registry, token, opts }) => {
5323
log.info('adduser', 'Polling for validated SSO session')
5424
return npmFetch.json(
5525
'/-/whoami', { ...opts, registry, forceAuth: { token } }
@@ -58,7 +28,7 @@ function pollForSession (registry, token, opts) {
5828
err => {
5929
if (err.code === 'E401') {
6030
return sleep(opts.ssoPollFrequency).then(() => {
61-
return pollForSession(registry, token, opts)
31+
return pollForSession({ registry, token, opts })
6232
})
6333
} else {
6434
throw err
@@ -70,3 +40,46 @@ function pollForSession (registry, token, opts) {
7040
function sleep (time) {
7141
return new Promise((resolve) => setTimeout(resolve, time))
7242
}
43+
44+
const login = async ({ creds, registry, scope }) => {
45+
log.warn('deprecated', 'SSO --auth-type is deprecated')
46+
47+
const opts = { ...npm.flatOptions, creds, registry, scope }
48+
const { ssoType } = opts
49+
50+
if (!ssoType) {
51+
throw new Error('Missing option: sso-type')
52+
}
53+
54+
// We're reusing the legacy login endpoint, so we need some dummy
55+
// stuff here to pass validation. They're never used.
56+
const auth = {
57+
username: 'npm_' + ssoType + '_auth_dummy_user',
58+
password: 'placeholder',
59+
email: 'support@npmjs.com',
60+
authType: ssoType
61+
}
62+
63+
const { token, sso } = await otplease(opts,
64+
opts => profile.loginCouch(auth.username, auth.password, opts)
65+
)
66+
67+
if (!token) { throw new Error('no SSO token returned') }
68+
if (!sso) { throw new Error('no SSO URL returned by services') }
69+
70+
await openUrl(sso, 'to complete your login please visit')
71+
72+
const username = await pollForSession({ registry, token, opts })
73+
74+
log.info('adduser', `Authorized user ${username}`)
75+
76+
const scopeMessage = scope ? ' to scope ' + scope : ''
77+
const message = `Logged in as ${username}${scopeMessage} on ${registry}.`
78+
79+
return {
80+
message,
81+
newCreds: { token }
82+
}
83+
}
84+
85+
module.exports = login

0 commit comments

Comments
 (0)