Passed
Push — master ( 31034c...d0aa87 )
by Aaron
01:57
created

test/src/test.ts   A

Complexity

Total Complexity 4
Complexity/F 0

Size

Lines of Code 388
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 4
eloc 273
mnd 4
bc 4
fnc 0
dl 0
loc 388
rs 10
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
import test from 'ava'
2
import ApiBuilder from 'claudia-api-builder'
3
import * as fs from 'fs'
4
import authenticator, {
5
    APIGatewayProxyEventJwt,
6
    authenticator as authenticator2,
7
    JwtHeader,
8
    Secret,
9
    SigningKeyCallback
10
} from '../../package/'
11
12
test('authenticator is exported', (t) => {
13
  t.is(typeof authenticator, 'function')
14
  t.is(authenticator, authenticator2)
15
})
16
17
/**
18
 * ## Using a Public Key
19
 *
20
 * Using a public key with an algorithm such as RS256 is recommended
21
 * for security and convenience. The public key can be kept inline with
22
 * your code without any security concerns.
23
 */
24
25
// tslint:disable-next-line:max-line-length
26
const TOKEN_RS512 = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.iNa6ZAKSn3J8GvG6hgfydtbRsKekSp1RunRceNhrRtUSZVvlSz4X1g1OXSJwCG2OyYXhaN0pyxuR7JlI1n663DnkEvop3T7Whxoy5uMxji9vSZ5MrvtLXY75On0ALqZuPuyuH4x6o1xI0huKHJGmRM2OVqD9W80AkpYtszwRwXkjiXdfJHMry9czXm5JrYNp9VowA9jATpkH2IatfSIVAK0c6hJg6Gz05PdtMjFwHpJJFn0qzfexf97pZgqITOX4f-pHZQ6i_jnBciocjMrn62tz8XpGrgjSGNqeUxROPjKCTnmwi0kpMAuBp2rUgj7Ns5wHaKE1riz_qrQqCgxrww'
27
28
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
29
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAry0bg77WbExsds8R4eJo
30
fNqbeWnu1QqRqG0wOk35JenMXDU6mCfUFas0ANgS/2PhxOoem5dtxKpJEzXF8eQh
31
xrO3J9zD9HMbLVMfodpG9Up9u+AUICGvMCAbAuCHcp7vTZtc+OmmSyk5qF1ApGnU
32
rWromBB8TDFVx0UdOR6I+1F3DvIk7mgjLAhwzycgsLRZFwXxS2mwHVAafD6QYbxZ
33
I655+ltaf3Gb3CBJSz888i3DfaKT30cCC/7r3rnOqbKjUcG8qxrsp+yOo8l6BeeJ
34
g57ITeuaRrSza7zdvS0Vydp9RS7VS9JdHQv9b48b7rsx+WLghI/AQ3kK0Xg85C9R
35
TQIDAQAB
36
-----END PUBLIC KEY-----`
37
38
test.cb('Authenticate with public key', (t) => {
39
  // Begin by creating your Api Builder as normal
40
  const api = new ApiBuilder()
41
  // Next pass in the authenticator along with your key and any config
42
  api.intercept(authenticator(PUBLIC_KEY))
43
  // Register your routes as normal
44
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
45
    // Export the proxyRouter normally in your code
46
    // export handler = api.proxyRouter
47
48
    // Here we call it instead to test it
49
  testApi(t, api, {
50
    context: { method: 'GET', path: '/greeting' },
51
    headers: { Authorization: 'bearer ' + TOKEN_RS512 }
52
  }, {
53
    body: '"Hello John Doe!"',
54
    statusCode: 200
55
  })
56
})
57
58
/**
59
 * ## Using a Secret Key
60
 *
61
 * When using a secret key you cannot safely include it in your code.
62
 * You must fetch the secret key securely at run time, either from a
63
 * file on you server or using a service like AWS Secrets Manager.
64
 *
65
 * Create a function that fetches the secret key like below,
66
 * and returns it via the callback or as a promise.
67
 * Be sure to decode from base64 if needed.
68
 */
69
70
// tslint:disable-next-line:max-line-length
71
const TOKEN_HS512 = 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Hk1Qgr18H-VwmDnMcljqEFy8_F1zeIVS-FY-3Xl2pKsMEeFii5-WEVDyBNRPredB9JjoNAkR23iOkTDN4Mu-Xg'
72
73
const getSecretKeyCb = (header: JwtHeader, callback: SigningKeyCallback) => {
74
  const filename = `test/test.secret.${header.alg.toLowerCase()}.b64.txt`
75
  fs.readFile(filename, (err, b64) => {
76
    if (err) {
77
      return callback(err)
78
    }
79
        // toString is important, if the first arg is a buffer the second is ignored
80
    const secret = Buffer.from(b64.toString(), 'base64')
81
    return callback(null, secret)
82
  })
83
}
84
85
const getSecretKeyP = (header: JwtHeader) =>
86
    new Promise((res, rej) => {
87
      getSecretKeyCb(header, (err, secret) => {
88
        if (err) {
89
          rej(err)
90
        } else {
91
          res(secret)
92
        }
93
      })
94
    })
95
96
test.cb('Authenticate with secret key, via promise', (t) => {
97
    // Begin by creating your Api Builder as normal
98
  const api = new ApiBuilder()
99
    // Next pass in the authenticator along with your key and any config
100
  api.intercept(authenticator(getSecretKeyP))
101
    // Register your routes as normal
102
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
103
    // Export the proxyRouter normally in your code
104
    // export handler = api.proxyRouter
105
106
    // Here we call it instead to test it
107
  testApi(t, api, {
108
    context: { method: 'GET', path: '/greeting' },
109
    headers: { Authorization: 'bearer ' + TOKEN_HS512 }
110
  }, {
111
    body: '"Hello John Doe!"',
112
    statusCode: 200
113
  })
114
})
115
116
test.cb('Authenticate with secret key, via callback', (t) => {
117
    // Begin by creating your Api Builder as normal
118
  const api = new ApiBuilder()
119
    // Next pass in the authenticator along with your key and any config
120
  api.intercept(authenticator(getSecretKeyCb))
121
    // Register your routes as normal
122
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
123
    // Export the proxyRouter normally in your code
124
    // export handler = api.proxyRouter
125
126
    // Here we call it instead to test it
127
  testApi(t, api, {
128
    context: { method: 'GET', path: '/greeting' },
129
    headers: { Authorization: 'bearer ' + TOKEN_HS512 }
130
  }, {
131
    body: '"Hello John Doe!"',
132
    statusCode: 200
133
  })
134
})
135
136
/**
137
 * ## JWT Headers & Signature
138
 *
139
 * You can access the headers, payload and signature of the JWT
140
 */
141
142
test.cb('Headers & Signature access', (t) => {
143
  const api = new ApiBuilder()
144
  api.intercept(authenticator(PUBLIC_KEY))
145
  api.get('/token', (event: APIGatewayProxyEventJwt) => ({
146
    algorithm: event.jwt.header.alg,
147
    signature: event.jwt.signature.substr(0, 12),
148
    subscriber: event.jwt.payload.sub
149
  }))
150
151
  testApi(t, api, {
152
    context: { method: 'GET', path: '/token' },
153
    headers: { Authorization: 'bearer ' + TOKEN_RS512 }
154
  }, {
155
    body: JSON.stringify({
156
      algorithm: 'RS512',
157
      signature: 'iNa6ZAKSn3J8',
158
      subscriber: '1234567890'
159
    }),
160
    statusCode: 200
161
  })
162
})
163
164
/**
165
 * ## Extra Config
166
 *
167
 * You can specify more arguments to increase security and help catch bugs
168
 *
169
 * For a full list see https://www.npmjs.com/package/jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback
170
 */
171
172
test.cb('Specify algorithm - success', (t) => {
173
  const api = new ApiBuilder()
174
  api.intercept(authenticator(PUBLIC_KEY, { algorithms: ['RS512'] }))
175
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
176
  testApi(t, api, {
177
    context: { method: 'GET', path: '/greeting' },
178
    headers: { Authorization: 'bearer ' + TOKEN_RS512 }
179
  }, {
180
    body: '"Hello John Doe!"',
181
    statusCode: 200
182
  })
183
})
184
185
test.cb('Specify algorithm - failure', (t) => {
186
  const api = new ApiBuilder()
187
  api.intercept(authenticator(PUBLIC_KEY, { algorithms: ['RS256'] }))
188
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
189
  testApi(t, api, {
190
    context: { method: 'GET', path: '/greeting' },
191
    headers: { Authorization: 'bearer ' + TOKEN_RS512 }
192
  }, {
193
    body: '"Unauthorised: JsonWebTokenError invalid algorithm"',
194
    statusCode: 401
195
  })
196
})
197
198
test.cb('Specify audience - success', (t) => {
199
    // tslint:disable-next-line:max-line-length
200
  const TOKEN_RS512_AUD = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhdWQiOiJGNDJFRDA4Mi03OTlGLTQ3NkItQjc3RS0xMjAxM0Y1Mzc5QTUifQ.VAnH9ozEAcL3foiSgqJspqS05AdYchn57uKrbCUEwX9uXbsg8nct9bL7y8Omw6qg5ZdTcNsnor8tysGW460yOmg06Pbx0SRHJifJGLpy1bOCWRPG_5NB5aM6uKf78T2QCJXm9f73nKfZ9QJUlfzW41bT2khnsO8gTVYo9yd3yesrKegMlSomxd4VrZFYz4jbNh2f9FUe8MNkubfOxVbM5U7sh5aZMs_uoef08Gxp3Aqx7fPpzj16uW2JTNlhoIYUF4J33T0SufgiR1Xw3R3Jn2BnwdlfgqjLrv0lxzDzHoPyPP8i6TSl3notTcTmLc_GItdcnLNPn8wtjxKNW81tMQ'
201
202
  const api = new ApiBuilder()
203
  api.intercept(authenticator(PUBLIC_KEY, { audience: 'F42ED082-799F-476B-B77E-12013F5379A5' }))
204
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
205
  testApi(t, api, {
206
    context: { method: 'GET', path: '/greeting' },
207
    headers: { Authorization: 'bearer ' + TOKEN_RS512_AUD }
208
  }, {
209
    body: '"Hello John Doe!"',
210
    statusCode: 200
211
  })
212
})
213
214
test.cb('Specify audience - failure', (t) => {
215
  const api = new ApiBuilder()
216
  api.intercept(authenticator(PUBLIC_KEY, { audience: 'F42ED082-799F-476B-B77E-12013F5379A5' }))
217
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
218
  testApi(t, api, {
219
    context: { method: 'GET', path: '/greeting' },
220
    headers: { Authorization: 'bearer ' + TOKEN_RS512 }
221
  }, {
222
    body: '"Unauthorised: JsonWebTokenError jwt audience invalid. expected: F42ED082-799F-476B-B77E-12013F5379A5"',
223
    statusCode: 401
224
  })
225
})
226
227
/**
228
 * ## When is a JWT not a JWT?
229
 */
230
231
test.cb('XWT', (t) => {
232
    // tslint:disable-next-line:max-line-length
233
  const TOKEN_RS512_XWT = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IlhXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.liqYAp9dbby9IzokHKbLkQb5mUAHoO0Ef7hn4Wz-Oh6kVcdqGQjtUFZPbONe9NPLLCuX_82_TkWXdKja5ISHLw3m028d2NGEQ2cbBxOCjHSqeuztUavwzQPeJUNUREh07IQK_MTw-BCskKLoJToIx2NZ3AfttCu4QWXBaJTZkIds0sQIQR11Z3w48QQS7Bjbtmrhzufpw_yfk8Fh0a0PjAlYmTgkE7JAUBT0NwqVPuqrTNUKxHe5DrqeuSAr0VeHSM05HvJFqrncF0KuBfTj0HUzwi6JpZxzlwxx_gzfaHEw2lFtRLJIonVLMAKM1aFj5m67FyfxqUQx0JnqRkEWrQ'
234
235
  const api = new ApiBuilder()
236
  api.intercept(authenticator(PUBLIC_KEY))
237
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
238
  testApi(t, api, {
239
    context: { method: 'GET', path: '/greeting' },
240
    headers: { Authorization: 'bearer ' + TOKEN_RS512_XWT }
241
  }, {
242
    body: '"Hello John Doe!"',
243
    statusCode: 200
244
  })
245
})
246
247
/**
248
 * ## Failures and Errors
249
 *
250
 * Here follow a number of error cases
251
 */
252
253
test.cb('Expired', (t) => {
254
    // tslint:disable-next-line:max-line-length
255
  const TOKEN_RS512_EXP = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.ed_PPzBaYmalmwTp1OxF9--QlPATgTWYVuc2Qg-tofDe4Mhn98B0aEZ-3wN9h2loQG05xhhUy_ZOyLYPYhZrKavU7UiVEIRmDUj2VYzmX575_GdGmxsaoluNP3xYqGjxs4U1-uQN1YIEQRvGx2pn-QeK9crawvzLVdZgyBr69-xVUbsDNIR5msx2Qg2uZLrPWe4ZGoYlpecUDfSoktHAkxsTfcjtE2niS_-Y8yoRqGemu8MWNwMca7edg2xJn-J0z5DDMYgzdVyI9oHkf-vu_lb535ekuYAigXBKLRBbPO9zzXv3LmJFlDJKJzkKGU8CSkUTR11ftsEc7BbUvsQ6Zg'
256
257
  const api = new ApiBuilder()
258
  api.intercept(authenticator(PUBLIC_KEY))
259
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
260
  testApi(t, api, {
261
    context: { method: 'GET', path: '/greeting' },
262
    headers: { Authorization: 'bearer ' + TOKEN_RS512_EXP }
263
  }, {
264
    body: '"Unauthorised: TokenExpiredError jwt expired"',
265
    statusCode: 401
266
  })
267
})
268
269
test.cb('Invalid/Forged token signature', (t) => {
270
    // tslint:disable-next-line:max-line-length
271
  const TOKEN_RS512_INV = 'eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.ed_PPzBaYmalmwTp1OxF9--QlPATgTWYVuc2Qg-tofDe4Mhn98B0aEZ-3wN9h2loQG05xhhUy_ZOyLYPYhZrKavU7UiVEIRmDUj2VYzmX575_GdGmxsaoluNP3xYqGjxs4U1-uQN1YIEQRvGx2pn-QeK9crawvzLVdZgyBr69-xVUbsDNIR5msx2Qg2uZLrPWe4ZGoYlpecUDfSoktHAkxsTfcjtE2niS_-Y8yoRqGemu8MWNwMca7edg2xJn-J0z5DDMYgzdVyI9oHkf-vu_lb535ekuYAigXBKLRBbPO9zzXv3LmJFlDJKJzkKGU7CSkUTR11ftsEc7BbUvsQ6Zg'
272
273
  const api = new ApiBuilder()
274
  api.intercept(authenticator(PUBLIC_KEY))
275
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
276
  testApi(t, api, {
277
    context: { method: 'GET', path: '/greeting' },
278
    headers: { Authorization: 'bearer ' + TOKEN_RS512_INV }
279
  }, {
280
    body: '"Unauthorised: JsonWebTokenError invalid signature"',
281
    statusCode: 401
282
  })
283
})
284
285
test.cb('Forgot to provide secret/key', (t) => {
286
  const api = new ApiBuilder()
287
  api.intercept(authenticator) // sic
288
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
289
  testApi(t, api, {
290
    context: { method: 'GET', path: '/greeting' },
291
    headers: { Authorization: 'bearer ' + TOKEN_RS512 }
292
  },
293
        undefined,
294
        'event does not contain routing information'
295
    )
296
})
297
298
const getSecretKeyPFail = () =>
299
    new Promise((res, rej) => {
300
      getSecretKeyCb({ alg: 'fail' }, (err, secret) => {
301
        if (err) {
302
          rej(err)
303
        } else {
304
          res(secret)
305
        }
306
      })
307
    })
308
309
test.cb('Failure fetching key', (t) => {
310
  const api = new ApiBuilder()
311
  api.intercept(authenticator(getSecretKeyPFail))
312
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
313
  testApi(t, api, {
314
    context: { method: 'GET', path: '/greeting' },
315
    headers: { Authorization: 'bearer ' + TOKEN_HS512 }
316
  }, {
317
    // tslint:disable-next-line:max-line-length
318
    body: '"Unauthorised: JsonWebTokenError error in secret or public key callback: ENOENT: no such file or directory, open \'test/test.secret.fail.b64.txt\'"',
319
    statusCode: 401
320
  })
321
})
322
323
test.cb('No headers in event', (t) => {
324
  const api = new ApiBuilder()
325
  api.intercept(authenticator(PUBLIC_KEY))
326
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
327
  testApi(t, api, {
328
    context: { method: 'GET', path: '/greeting' }
329
  }, {
330
    body: '"Unauthorised: no headers"',
331
    statusCode: 401
332
  })
333
})
334
335
test.cb('No authorization in headers', (t) => {
336
  const api = new ApiBuilder()
337
  api.intercept(authenticator(PUBLIC_KEY))
338
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
339
  testApi(t, api, {
340
    context: { method: 'GET', path: '/greeting' },
341
    headers: {}
342
  }, {
343
    body: '"Unauthorised: no authorization header"',
344
    statusCode: 401
345
  })
346
})
347
348
test.cb('No bearer in authorization header', (t) => {
349
  const api = new ApiBuilder()
350
  api.intercept(authenticator(PUBLIC_KEY))
351
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
352
  testApi(t, api, {
353
    context: { method: 'GET', path: '/greeting' },
354
    headers: { Authorization: 'Basic QWxhZGRpbjpPcGVuU2VzYW1l' }
355
  }, {
356
    body: '"Unauthorised: authorization scheme must be bearer"',
357
    statusCode: 401
358
  })
359
})
360
361
test.cb('No token in authorization header', (t) => {
362
  const api = new ApiBuilder()
363
  api.intercept(authenticator(PUBLIC_KEY))
364
  api.get('/greeting', (event: APIGatewayProxyEventJwt) => `Hello ${event.jwt.payload.name}!`)
365
  testApi(t, api, {
366
    context: { method: 'GET', path: '/greeting' },
367
    headers: { Authorization: 'bearer ' }
368
  }, {
369
    body: '"Unauthorised: no authorization token"',
370
    statusCode: 401
371
  })
372
})
373
374
const testApi = (t: any, api: any, event: any, expectedResponse: any, expectedError: any = null) => {
375
  t.plan(1)
376
  const done = async (error: any, response: any) => {
377
        // await error
378
        // await response
379
    if (response) delete response.headers
380
    t.deepEqual(
381
            { error, response },
382
            { error: expectedError, response: expectedResponse }
383
        )
384
    t.end()
385
  }
386
  api.proxyRouter(event, { done }, done)
387
}
388