/
index.ts
189 lines (163 loc) · 4.85 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import * as url from 'url';
import * as open from 'open';
import * as uuid from 'uuid';
import * as Debug from 'debug';
import { Spinner } from 'cli-spinner';
import * as snyk from '../../../lib';
import { verifyAPI } from './is-authed';
import { isCI } from '../../../lib/is-ci';
import * as config from '../../../lib/config';
import request = require('../../../lib/request');
import { CustomError } from '../../../lib/errors';
import { AuthFailedError } from '../../../lib/errors';
import { TokenExpiredError } from '../../../lib/errors/token-expired-error';
import { MisconfiguredAuthInCI } from '../../../lib/errors/misconfigured-auth-in-ci-error';
import { Payload } from '../../../lib/request/types';
import { getQueryParamsAsString } from '../../../lib/query-strings';
export = auth;
const apiUrl = url.parse(config.API);
const authUrl = apiUrl.protocol + '//' + apiUrl.host;
const debug = Debug('snyk-auth');
let attemptsLeft = 0;
function resetAttempts() {
attemptsLeft = 30;
}
type AuthCliCommands = 'wizard' | 'ignore';
async function webAuth(via: AuthCliCommands) {
const token = uuid.v4(); // generate a random key
const redirects = {
wizard: '/authenticated',
};
let urlStr = authUrl + '/login?token=' + token;
const utmParams = getQueryParamsAsString();
if (utmParams) {
urlStr += '&' + utmParams;
}
// validate that via comes from our code, and not from user & CLI
if (redirects[via]) {
urlStr += '&redirectUri=' + Buffer.from(redirects[via]).toString('base64');
}
const msg =
'\nNow redirecting you to our auth page, go ahead and log in,\n' +
"and once the auth is complete, return to this prompt and you'll\n" +
"be ready to start using snyk.\n\nIf you can't wait use this url:\n" +
urlStr +
'\n';
// suppress this message in CI
if (!isCI()) {
console.log(msg);
} else {
return Promise.reject(MisconfiguredAuthInCI());
}
const spinner = new Spinner('Waiting...');
spinner.setSpinnerString('|/-\\');
const ipFamily = await getIpFamily();
try {
spinner.start();
await setTimeout(() => {
open(urlStr);
}, 0);
return await testAuthComplete(token, ipFamily);
} finally {
spinner.stop(true);
}
}
async function testAuthComplete(
token: string,
ipFamily?: number,
): Promise<{ res; body }> {
const payload: Partial<Payload> = {
body: {
token,
},
url: config.API + '/verify/callback',
json: true,
method: 'post',
};
if (ipFamily) {
payload.family = ipFamily;
}
return new Promise((resolve, reject) => {
debug(payload);
request(payload, (error, res, body) => {
debug(error, (res || {}).statusCode, body);
if (error) {
return reject(error);
}
if (res.statusCode !== 200) {
return reject(errorForFailedAuthAttempt(res, body));
}
// we have success
if (body.api) {
return resolve({
res,
body,
});
}
// we need to wait and poll again in a moment
setTimeout(() => {
attemptsLeft--;
if (attemptsLeft > 0) {
return resolve(testAuthComplete(token, ipFamily));
}
reject(TokenExpiredError());
}, 1000);
});
});
}
async function auth(apiToken: string, via: AuthCliCommands): Promise<string> {
let promise;
resetAttempts();
if (apiToken) {
// user is manually setting the API token on the CLI - let's trust them
promise = verifyAPI(apiToken);
} else {
promise = webAuth(via);
}
return promise.then((data) => {
const res = data.res;
const body = res.body;
debug(body);
if (res.statusCode === 200 || res.statusCode === 201) {
snyk.config.set('api', body.api);
return (
'\nYour account has been authenticated. Snyk is now ready to ' +
'be used.\n'
);
}
throw errorForFailedAuthAttempt(res, body);
});
}
/**
* Resolve an appropriate error for a failed attempt to authenticate
*
* @param res The response from the API
* @param body The body of the failed authentication request
*/
function errorForFailedAuthAttempt(res, body) {
if (res.statusCode === 401 || res.statusCode === 403) {
return AuthFailedError(body.userMessage, res.statusCode);
} else {
const userMessage = body && body.userMessage;
const error = new CustomError(userMessage || 'Auth request failed');
if (userMessage) {
error.userMessage = userMessage;
}
error.code = res.statusCode;
return error;
}
}
async function getIpFamily(): Promise<6 | undefined> {
const family = 6;
try {
// Dispatch a FORCED IPv6 request to test client's ISP and network capability
await request({
url: config.API + '/verify/callback',
family, // family param forces the handler to dispatch a request using IP at "family" version
method: 'post',
});
return family;
} catch (e) {
return undefined;
}
}