-
Notifications
You must be signed in to change notification settings - Fork 0
/
getToken.test.ts
275 lines (219 loc) · 8.8 KB
/
getToken.test.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
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
import express from 'express'
import passport from 'passport'
import { expect } from 'chai'
import { getUser, seedUsers } from '../../helpers'
import { initAuth, PasswordReset } from '../../../src'
import { UserRepository } from '../../mocks/repositories/userRepository'
import { PasswordResetTokenRepository } from '../../mocks/repositories/passwordResetTokenRepository'
import { RefreshTokenRepository } from '../../mocks/repositories/refreshTokenRepository'
// import { LoginUser, LoginUserProperty, loginUsers } from '../../seeds/users'
/*
function percentage(count: number, base: number): string {
return `${((count / base) * 100).toFixed(2)}%`
}
function getNonExistingUser(): LoginUser {
const user = loginUsers.getNegativeUser([LoginUserProperty.NON_EXISTING])
if (!user) {
throw new Error('no negative user')
}
return user
}
*/
function declareTests() {
it('Non existing email', async () => {
const result = await PasswordReset.getToken('nonexisting@email.com')
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(result).to.be.undefined
})
it('Valid email', async () => {
const user = getUser()
const result = await PasswordReset.getToken(user.email)
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(result).to.exist
})
}
describe('Password reset: getToken method', () => {
const userRepo = new UserRepository()
const passwordResetTokenRepo = new PasswordResetTokenRepository()
const app = express()
before(async () => {
await seedUsers(userRepo)
// init authentication library
initAuth(passport, {
userRepository: userRepo,
refreshTokenRepository: RefreshTokenRepository.getInstance(),
passwordResetTokenRepository: passwordResetTokenRepo
})
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
})
declareTests()
})
describe('Password reset: getToken method without password reset token repository', () => {
const userRepo = new UserRepository()
const app = express()
before(async () => {
await seedUsers(userRepo)
// init authentication library
initAuth(passport, {
userRepository: userRepo,
refreshTokenRepository: RefreshTokenRepository.getInstance()
})
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
})
declareTests()
})
// Tests are not passing just yet (mainly on the GH actions)
// NOTE: issue link: https://github.com/Slonik923/passport-jwt-wrapper/issues/6
/*
function declareTimingAttack() {
it('Timing attack', async () => {
// test variables
let iterations = 10000
const allowedAverageDifference = 0.2 // 20%
const invalidTimes: number[] = []
const validTimes: number[] = []
// do not measure first x times, since they are slow due to the JIT compilation
// eslint-disable-next-line no-plusplus
for (let i = 0; i < iterations; i++) {
const user = getUser()
const nonExistingUser = getNonExistingUser()
// eslint-disable-next-line no-await-in-loop
await PasswordReset.getToken(user.email)
// eslint-disable-next-line no-await-in-loop
await PasswordReset.getToken(nonExistingUser.email)
}
const movingSize = 50
const movingTime: number[] = Array(movingSize) // last 100 execution times
let validTime = 0
let invalidTime = 0
const suggestedValid: number[] = []
const suggestedInvalid: number[] = []
// NOTE: need to test just a small number of iterations, since compiler is too smart
// when running 1000 times, last iterations are 5x times faster than average ...
iterations /= 10
// eslint-disable-next-line no-plusplus
for (let i = 0; i < iterations; i++) {
const nonExistingUser = getNonExistingUser()
let startTime = performance.now()
// eslint-disable-next-line no-await-in-loop
await PasswordReset.getToken(nonExistingUser.email)
let endTime = performance.now()
let time = endTime - startTime
// increment invalidTime
invalidTime += time
// add to moving time
if (movingTime.length === movingSize) {
movingTime.shift()
}
movingTime.push(time)
// calculate moving average from moving time
let avg = movingTime.reduce((prev, next) => prev + next, 0) / movingTime.length
if (time < avg * (1 - allowedAverageDifference)) {
suggestedInvalid.push(time)
// console.log(`[invalid] smaller than avg: ${time.toFixed(4)}ms (${avg.toFixed(4)})`)
}
if (time > avg * (1 + allowedAverageDifference)) {
suggestedValid.push(time)
// console.log(`[invalid] bigger than avg: ${time.toFixed(4)}ms (${avg.toFixed(4)})`)
}
const user = getUser()
startTime = performance.now()
// eslint-disable-next-line no-await-in-loop
await PasswordReset.getToken(user.email)
endTime = performance.now()
time = endTime - startTime
// increment validTime
validTime += time
// add to moving time
if (movingTime.length === movingSize) {
movingTime.shift()
}
movingTime.push(time)
// calculate moving average from moving time
avg = movingTime.reduce((prev, next) => prev + next, 0) / movingTime.length
if (time < avg * (1 - allowedAverageDifference)) {
suggestedInvalid.push(time)
// console.log(`[valid] smaller than avg: ${time.toFixed(4)}ms (${avg.toFixed(4)})`)
}
if (time > avg * (1 + allowedAverageDifference)) {
suggestedValid.push(time)
// console.log(`[valid] bigger than avg: ${time.toFixed(4)}ms (${avg.toFixed(4)})`)
}
}
const invalidAvg = invalidTime / iterations
const validAvg = validTime / iterations
const all = [...invalidTimes, ...validTimes]
const avg = (invalidTime + validTime) / (iterations * 2)
console.log(`average execution time: ${avg.toFixed(4)}ms`)
console.log(`average invalid execution time: ${invalidAvg.toFixed(4)}ms`)
console.log(`average valid execution time: ${validAvg.toFixed(4)}ms`)
// difference between valid average and invalid average should be less than 10%
expect(validAvg - invalidAvg).to.lt(avg * allowedAverageDifference)
// guess which values could be valid and invalid
all.forEach((val) => {
if (val > avg * (1 + allowedAverageDifference)) {
suggestedValid.push(val)
} else if (val < avg * (1 - allowedAverageDifference)) {
suggestedInvalid.push(val)
}
})
// NOTE: times * 2, since each execution we tested both valid and invalid value
console.log('suggested valid', suggestedValid.length, percentage(suggestedValid.length, iterations))
console.log('suggested invalid', suggestedInvalid.length, percentage(suggestedInvalid.length, iterations))
console.log(
'suggested total',
suggestedInvalid.length + suggestedValid.length,
percentage(suggestedInvalid.length + suggestedValid.length, iterations * 2)
)
// no more 30% executions should be identifiable (their execution time differs from average by more, than allowed difference)
expect(suggestedValid.length).to.lt(iterations * 2 * 0.3)
expect(suggestedInvalid.length).to.lt(iterations * 2 * 0.3)
expect(suggestedValid.length + suggestedInvalid.length).to.lt(iterations * 2 * 0.3)
// filter wrongly suggested values
const guessedValid = suggestedValid.filter((val) => validTimes.indexOf(val) > 0)
const guessedInvalid = suggestedInvalid.filter((val) => invalidTimes.indexOf(val) > 0)
console.log('guessed valid:', guessedValid.length, percentage(guessedValid.length, iterations))
console.log('guessed invalid:', guessedInvalid.length, percentage(guessedInvalid.length, iterations))
console.log('guessed total', guessedInvalid.length + guessedValid.length, percentage(guessedInvalid.length + guessedValid.length, iterations * 2))
// no more than 10 % of the guessed executions should actually be true
expect(guessedValid.length).to.lt(iterations * 2 * 0.1)
expect(guessedInvalid.length).to.lt(iterations * 2 * 0.1)
// no more than 15% of the combined guessed executions should actually be true
expect(guessedValid.length + guessedInvalid.length).to.lt(iterations * 2 * 0.15)
})
}
describe('Password reset: getToken time with passwordResetRepository', () => {
const userRepo = new UserRepository()
const passwordResetTokenRepo = new PasswordResetTokenRepository()
const app = express()
before(async () => {
await seedUsers(userRepo)
// init authentication library
initAuth(passport, {
userRepository: userRepo,
refreshTokenRepository: TokenRepository.getInstance(),
passwordResetTokenRepository: passwordResetTokenRepo
})
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
})
declareTimingAttack()
})
describe('Password reset: getToken time w/o passwordResetRepository', () => {
const userRepo = new UserRepository()
const app = express()
before(async () => {
await seedUsers(userRepo)
// init authentication library
initAuth(passport, {
userRepository: userRepo,
refreshTokenRepository: TokenRepository.getInstance()
})
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
})
declareTimingAttack()
})
*/