-
Notifications
You must be signed in to change notification settings - Fork 0
/
authpoint-demo.js
346 lines (325 loc) · 11 KB
/
authpoint-demo.js
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
const qs = require('qs')
const axios = require('axios')
var assert = require('assert')
const dotenv = require('dotenv')
const prompt = require('prompt-sync')()
const keypress = require('keypress')
const {
uptime
} = require('process')
dotenv.config()
const NOISE = true
const QUERY_WAIT_CYCLES = 6 // this is how many times we will query the API asking if the user has answered - silly really.
const APITIMER = ms => new Promise(res => setTimeout(res, ms))
class AuthpointResources {
constructor() {
NOISE && console.log('::AuthpointResources')
this.resourceId = process.env.APRESOURCEID
this.accountId = process.env.ACCOUNTID
this.api_key = process.env.APAPIKEY
this.base_api_url = 'https://api.usa.cloud.watchguard.com/rest/authpoint/authentication/v1/accounts'
}
}
const TRANSACTION_HEADERS = {
headers: {
'Authorization': '',
'Content-Type': 'application/json',
'WatchGuard-API-Key': ''
}
}
class WatchGuardAuthpoint extends AuthpointResources {
constructor(username, origin) {
NOISE && console.log('::WatchGuardAuthpoint')
super()
assert.strictEqual(typeof (username), typeof (''))
assert.strictEqual(typeof (origin), typeof (''))
this.username = username
this.origin = origin
this.bearer = null
// refresh every fiftyfive minutes but the API expires the authentication tokens hourly, set as needed
this.__refreshTimer = setInterval(() => {
this.refreshBearerToken()
}, 55 * (60 * 1000))
this.getBearerToken()
}
__ready = false
__refreshTimer = null
__allowPush = null
__allowAuthenticate = null
__allowOTP = null
__authHeaders = () => {
TRANSACTION_HEADERS.headers['Authorization'] = 'Bearer ' + this.bearer
TRANSACTION_HEADERS.headers['WatchGuard-API-Key'] = this.api_key
return TRANSACTION_HEADERS
}
__OAuth = () => {
return Buffer.from(`${process.env.ACCESSID_RW}:${process.env.WGCPASSWORD}`, 'utf8').toString('base64')
}
/**
* returns {true} if the bearer token has been claimed,
* {false} if a refresh is in progress or the token is blank
*
* @memberof WatchGuardAuthpoint
*/
ready = () => {
return this.__ready
}
/**
* sets an interval timer to refresh the bearer each hour
*
* @date 2021-05-20
* @memberof WatchGuardAuthpoint
*/
async refreshBearerToken() {
NOISE && console.log('::refreshBearerToken')
clearInterval(this.__refreshTimer)
await this.getBearerToken()
this.__refreshTimer = setInterval(() => {
this.refreshBearerToken()
}, 55 * (60 * 1000))
}
/**
* refreshes the bearer token which expires after one-hour
*
* @date 2021-05-20
* @memberof WatchGuardAuthpoint
*/
async getBearerToken() {
await axios
.post(
'https://api.usa.cloud.watchguard.com/oauth/token',
qs.stringify({
grant_type: 'client_credentials',
scope: 'api-access'
}), {
headers: {
Authorization: `Basic ${this.__OAuth()}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
}
)
.then((res) => {
NOISE && console.log(`::bearer ...${res.data.access_token.slice(-(15))}`)
this.bearer = res.data.access_token
this.__ready = true
})
.catch((error) => {
throw error
})
}
/**
* gets the users authentication policy with the authentication policy JSON (userAuthPolicyResult.json)
*
* @date 2021-05-20
* @returns JSON block (userAuthPolicyResult.json)
* @memberof WatchGuardAuthpoint
*/
async getUserAuthenticationPolicy() {
assert.strictEqual(typeof (this.bearer), typeof (''))
var data = JSON.stringify({
"login": this.username,
"originIpAddress": this.origin
})
return new Promise((resolve, reject) => {
axios.post(`${this.base_api_url}/${this.accountId}/resources/${this.resourceId}/authenticationpolicy`, data, this.__authHeaders())
.then(res => {
this.__allowAuthenticate = res.data.isAllowedToAuthenticate
NOISE && console.log('isAllowedToAuthenticate: ' + this.__allowAuthenticate)
this.__allowPush = res.data.policyResponse.push
NOISE && console.log('policyResponse::push: ' + this.__allowPush)
this.__allowOTP = res.data.policyResponse.otp
NOISE && console.log('policyResponse::otp: ' + this.__allowOTP)
resolve(res.data)
})
.catch(error => {
console.log(error);
reject()
})
})
}
/**
* sends a push notification to the end user provided they are allowed to receive push notifications
* as checked by the user policy
*
* @date 2021-05-20
* @param {*} data
* @returns {transactionId}
* @memberof WatchGuardAuthpoint
*/
async sendAuthenticationPush(data) {
NOISE && console.log('::sendAuthenticationPush')
if (!this.__allowAuthenticate) {
console.log('This user is not allowed to authenticate with authpoint.');
return
}
if (!this.__allowPush) {
console.log('This user is not allowed to authenticate through push.');
return
}
assert.strictEqual(typeof (this.bearer), typeof (''))
return new Promise((resolve, reject) => {
axios.post(`${this.base_api_url}/${this.accountId}/resources/${this.resourceId}/transactions`, data, this.__authHeaders())
.then(res => {
NOISE && console.log(`::sendAuthenticationPush - ${res.data.transactionId}`)
resolve(res.data.transactionId)
})
.catch(error => {
console.log(error);
reject()
})
})
}
/**
* polling mechinism to query API to see if the user has responded yet
*
* @date 2021-05-20
* @param {*} transactionId
* @returns
* @memberof WatchGuardAuthpoint
*/
async requestTransactionIDResult(transactionId) {
assert.strictEqual(typeof (arguments[0]), typeof (''))
assert.strictEqual(typeof (this.bearer), typeof (''))
try {
const res = await axios.get(`${this.base_api_url}/${this.accountId}/resources/${this.resourceId}/transactions/${transactionId}`, this.__authHeaders())
return res.data
} catch (err) {
if (err.response.status == 403) {
return {
pushResult: 'DENIED'
}
}
}
}
/**
* attempt to authenticate against an OTP
*
* @date 2021-05-31
* @param {*} data
* @returns res.data JSON response either
* { authenticationResult: 'AUTHORIZED' } or
* { "title": "[Authentication] MFA did not authorize", }
* why they don't just use 'auth': true or 'auth': false I have no idea...
* @memberof WatchGuardAuthpoint
*/
async authenticateWithOTP(data) {
NOISE && console.log('::authenticateWithOTP')
if (!this.__allowAuthenticate) {
console.log('This user is not allowed to authenticate with authpoint.');
return
}
if (!this.__allowOTP) {
console.log('This user is not allowed to authenticate with an OTP code.');
return
}
assert.strictEqual(typeof (this.bearer), typeof (''))
return new Promise((resolve, reject) => {
axios.post(`${this.base_api_url}/${this.accountId}/resources/${this.resourceId}/otp`, data, this.__authHeaders())
.then(res => {
resolve(res.data)
})
.catch(error => {
console.log(error);
reject()
})
})
}
}
const args = process.argv.slice(2) ?? null
if (!args[0] || !args[1]) {
console.log('username origin required');
process.exit()
}
let __wg = new WatchGuardAuthpoint(args[0], args[1])
keypress(process.stdin)
process.stdin.on('keypress', async function (ch, key) {
if (key.name == 'f1') {
process.exit()
}
if (key.name == 'f2') {
if (__wg.ready()) {
var data = JSON.stringify({
'login': args[0],
'type': 'PUSH',
'originIpAddress': args[1],
'clientInfoRequest': {
'machineName': '',
'osVersion': '',
'domain': ''
}
})
//# get the users policy to see if they are allowed to auth/push
await __wg.getUserAuthenticationPolicy().then(async res => {
NOISE && console.log('got policy.')
}).catch(error => {
throw 'Failed to get users auth policy.'
})
await __wg.sendAuthenticationPush(data)
.then(async transactionId => {
NOISE && console.log('transaction pushed, awaiting users response...')
// within the first 1000ms, the WG API will return a series of useless status flags so I added an intentional
// delay of 1.5 seconds before I begin polling the sytem
await APITIMER(1500)
let iterations = 0,
res
while (iterations++ < QUERY_WAIT_CYCLES) {
res = await __wg.requestTransactionIDResult(transactionId)
if (res?.status == 202 && res?.title.includes('processing')) console.log(res.title) // can remove these - just for affect
if (res?.status == 202 && res?.title.includes('device')) console.log(res.title) // can remove these - just for affect
// the user authorized - notice the WG API uses actual text, not status codes to reflect the action
if (res?.pushResult == 'AUTHORIZED') {
console.log('User Authorized Push');
break;
}
// the user denied the transaction - the DENIED comes from the method, NOT the WG API
if (res?.pushResult == 'DENIED') {
console.log('User Denied Push');
break;
}
// literally, do nothing while waiting for the user
if (iterations < 4) await APITIMER(3500)
}
NOISE && console.log('no longer checking, I hope you got a good result..')
})
.catch(error => {
throw error
})
} else {
console.log('wait for a bearer refresh or give up hope.')
}
}
if (key.name == 'f3') {
if (__wg.ready()) {
let otp = prompt('Enter the current OTP in app or from your key-fob: ')
if (otp.length <= 6) {
var data = JSON.stringify({
'login': args[0],
'otp': otp,
'originIpAddress': args[1]
})
//# get the users policy to see if they are allowed to auth/push
await __wg.getUserAuthenticationPolicy().then(async res => {
NOISE && console.log('got policy.')
}).catch(error => {
throw 'Failed to get users auth policy.'
})
await __wg.authenticateWithOTP(data).then((res) => {
if (res?.authenticationResult == 'AUTHORIZED') {
console.log('OPT was verified and authorized.')
} else {
console.log('OPT failed to authorize.')
}
})
} else {
console.log('otp codes cannot be longer than six numbers, try again.')
}
} else {
console.log('wait for a bearer refresh or give up hope.')
}
}
})
console.log('Press F3 to use an OTP (keyfob)')
console.log('Press F2 to send an authpoint authentication push notification')
console.log('Press F1 to terminate')
process.stdin.setRawMode(true)
process.stdin.resume()