UNPKG

5.98 kBJavaScriptView Raw
1'use strict'
2
3const co = require('co')
4const cli = require('..')
5const vars = require('./vars')
6
7function basicAuth (username, password) {
8 let auth = [username, password].join(':')
9 auth = Buffer.from(auth).toString('base64')
10 return `Basic ${auth}`
11}
12
13function createOAuthToken (username, password, expiresIn, secondFactor) {
14 const os = require('os')
15
16 let headers = {
17 Authorization: basicAuth(username, password)
18 }
19
20 if (secondFactor) headers['Heroku-Two-Factor-Code'] = secondFactor
21
22 return cli.heroku.post('/oauth/authorizations', {
23 headers,
24 body: {
25 scope: ['global'],
26 description: `Heroku CLI login from ${os.hostname()} at ${new Date()}`,
27 expires_in: expiresIn || 60 * 60 * 24 * 365 // 1 year
28 }
29 }).then(function (auth) {
30 return {token: auth.access_token.token, email: auth.user.email, expires_in: auth.access_token.expires_in}
31 })
32}
33
34function saveToken ({email, token}) {
35 const netrc = require('netrc-parser').default
36 netrc.loadSync()
37 const hosts = [vars.apiHost, vars.httpGitHost]
38 hosts.forEach(host => {
39 if (!netrc.machines[host]) netrc.machines[host] = {}
40 netrc.machines[host].login = email
41 netrc.machines[host].password = token
42 })
43 netrc.saveSync()
44}
45
46function * loginUserPass ({save, expires_in}) {
47 const {prompt} = require('./prompt')
48
49 cli.log('Enter your Heroku credentials:')
50 let email = yield prompt('Email')
51 let password = yield prompt('Password', {hide: true})
52
53 let auth
54 try {
55 auth = yield createOAuthToken(email, password, expires_in)
56 } catch (err) {
57 if (!err.body || err.body.id !== 'two_factor') throw err
58 let secondFactor = yield prompt('Two-factor code', {mask: true})
59 auth = yield createOAuthToken(email, password, expires_in, secondFactor)
60 }
61 if (save) saveToken(auth)
62 return auth
63}
64
65function * loginSSO ({save, browser}) {
66 const {prompt} = require('./prompt')
67
68 let url = process.env['SSO_URL']
69 if (!url) {
70 let org = process.env['HEROKU_ORGANIZATION']
71 if (!org) {
72 org = yield prompt('Enter your organization name')
73 }
74 url = `https://sso.heroku.com/saml/${encodeURIComponent(org)}/init?cli=true`
75 }
76
77 const open = require('./open')
78
79 let openError
80 yield cli.action('Opening browser for login', open(url, browser)
81 .catch(function (err) {
82 openError = err
83 })
84 )
85
86 if (openError) {
87 cli.console.error(openError.message)
88 }
89
90 let token = yield prompt('Enter your access token (typing will be hidden)', {hide: true})
91
92 let account = yield cli.heroku.get('/account', {
93 headers: {
94 Authorization: `Bearer ${token}`
95 }
96 })
97
98 if (save) saveToken({token, email: account.email})
99 return {token: token, email: account.email}
100}
101
102function * logout () {
103 let token = cli.heroku.options.token
104 if (token) {
105 // for SSO logins we delete the session since those do not show up in
106 // authorizations because they are created a trusted client
107 let sessionsP = cli.heroku.delete('/oauth/sessions/~')
108 .catch(err => {
109 if (err.statusCode === 404 && err.body && err.body.id === 'not_found' && err.body.resource === 'session') {
110 return null
111 }
112 if (err.statusCode === 401 && err.body && err.body.id === 'unauthorized') {
113 return null
114 }
115 throw err
116 })
117
118 // grab the default authorization because that is the token shown in the
119 // dashboard as API Key and they may be using it for something else and we
120 // would unwittingly break an integration that they are depending on
121 let defaultAuthorizationP = cli.heroku.get('/oauth/authorizations/~')
122 .catch(err => {
123 if (err.statusCode === 404 && err.body && err.body.id === 'not_found' && err.body.resource === 'authorization') {
124 return null
125 }
126 if (err.statusCode === 401 && err.body && err.body.id === 'unauthorized') {
127 return null
128 }
129 throw err
130 })
131
132 // grab all the authorizations so that we can delete the token they are
133 // using in the CLI. we have to do this rather than delete ~ because
134 // the ~ is the API Key, not the authorization that is currently requesting
135 let authorizationsP = cli.heroku.get('/oauth/authorizations')
136 .catch(err => {
137 if (err.statusCode === 401 && err.body && err.body.id === 'unauthorized') {
138 return []
139 }
140 throw err
141 })
142
143 let [, defaultAuthorization, authorizations] = yield [sessionsP, defaultAuthorizationP, authorizationsP]
144
145 if (accessToken(defaultAuthorization) !== token) {
146 for (let authorization of authorizations) {
147 if (accessToken(authorization) === token) {
148 // remove the matching access token from core services
149 yield cli.heroku.delete(`/oauth/authorizations/${authorization.id}`)
150 }
151 }
152 }
153 }
154
155 const netrc = require('netrc-parser').default
156 netrc.loadSync()
157 if (netrc.machines[vars.apiHost]) {
158 netrc.machines[vars.apiHost] = undefined
159 }
160 if (netrc.machines[vars.httpGitHost]) {
161 netrc.machines[vars.httpGitHost] = undefined
162 }
163 netrc.saveSync()
164}
165
166function accessToken (authorization) {
167 return authorization && authorization.access_token && authorization.access_token.token
168}
169
170function * login (options = {}) {
171 yield logout()
172
173 try {
174 if (options['sso']) {
175 return yield loginSSO(options)
176 } else {
177 return yield loginUserPass(options)
178 }
179 } catch (e) {
180 const {PromptMaskError} = require('./prompt')
181 const os = require('os')
182 if (e instanceof PromptMaskError && os.platform() === 'win32') {
183 throw new PromptMaskError('Login is currently incompatible with git bash/Cygwin/MinGW')
184 } else {
185 throw e
186 }
187 }
188}
189
190function token () {
191 const netrc = require('netrc-parser').default
192 netrc.loadSync()
193 if (process.env.HEROKU_API_KEY) return process.env.HEROKU_API_KEY
194 return netrc.machines[vars.apiHost] && netrc.machines[vars.apiHost].password
195}
196
197module.exports = {
198 login: co.wrap(login),
199 logout: co.wrap(logout),
200 token
201}