1 | 'use strict'
|
2 |
|
3 | const co = require('co')
|
4 | const cli = require('..')
|
5 | const vars = require('./vars')
|
6 |
|
7 | function basicAuth (username, password) {
|
8 | let auth = [username, password].join(':')
|
9 | auth = Buffer.from(auth).toString('base64')
|
10 | return `Basic ${auth}`
|
11 | }
|
12 |
|
13 | function 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
|
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 |
|
34 | function 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 |
|
46 | function * 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 |
|
65 | function * 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 |
|
102 | function * logout () {
|
103 | let token = cli.heroku.options.token
|
104 | if (token) {
|
105 |
|
106 |
|
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 |
|
119 |
|
120 |
|
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 |
|
133 |
|
134 |
|
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 |
|
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 |
|
166 | function accessToken (authorization) {
|
167 | return authorization && authorization.access_token && authorization.access_token.token
|
168 | }
|
169 |
|
170 | function * 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 |
|
190 | function 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 |
|
197 | module.exports = {
|
198 | login: co.wrap(login),
|
199 | logout: co.wrap(logout),
|
200 | token
|
201 | }
|