├── .github
└── workflows
│ └── ci-plugin.yml
├── .gitignore
├── API.md
├── LICENSE.md
├── README.md
├── lib
├── crypto.js
├── index.d.ts
├── index.js
├── keys.js
├── plugin.js
├── token.js
└── utils.js
├── package.json
└── test
├── crypto.js
├── esm.js
├── keys.js
├── mock.js
├── plugin.js
├── token.js
└── utils.js
/.github/workflows/ci-plugin.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | jobs:
11 | test:
12 | uses: hapijs/.github/.github/workflows/ci-plugin.yml@master
13 | with:
14 | min-node-version: 14
15 | min-hapi-version: 20
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/package-lock.json
3 |
4 | coverage.*
5 |
6 | **/.DS_Store
7 | **/._*
8 |
9 | **/*.pem
10 |
11 | **/.vs
12 | **/.vscode
13 | **/.idea
14 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 | **jwt** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together.
3 |
4 | ## Usage
5 | ```js
6 | // Load modules
7 |
8 | const Jwt = require('@hapi/jwt');
9 | const Hapi = require('@hapi/hapi');
10 |
11 | // Declare internals
12 |
13 | const internals = {};
14 |
15 | internals.start = async function () {
16 |
17 | const server = Hapi.server({ port: 8000 });
18 |
19 | // Register jwt with the server
20 |
21 | await server.register(Jwt);
22 |
23 | // Declare an authentication strategy using the jwt scheme.
24 | // Use keys: with a shared secret key OR json web key set uri.
25 | // Use verify: To determine how key contents are verified beyond signature.
26 | // If verify is set to false, the keys option is not required and ignored.
27 | // The verify: { aud, iss, sub } options are required if verify is not set to false.
28 | // The verify: { exp, nbf, timeSkewSec, maxAgeSec } parameters have defaults.
29 | // Use validate: To create a function called after token validation.
30 |
31 | server.auth.strategy('my_jwt_strategy', 'jwt', {
32 | keys: 'some_shared_secret',
33 | verify: {
34 | aud: 'urn:audience:test',
35 | iss: 'urn:issuer:test',
36 | sub: false,
37 | nbf: true,
38 | exp: true,
39 | maxAgeSec: 14400, // 4 hours
40 | timeSkewSec: 15
41 | },
42 | validate: (artifacts, request, h) => {
43 |
44 | return {
45 | isValid: true,
46 | credentials: { user: artifacts.decoded.payload.user }
47 | };
48 | }
49 | });
50 |
51 | // Set the strategy
52 |
53 | server.auth.default('my_jwt_strategy');
54 | };
55 |
56 | internals.start();
57 | ```
58 | ### server.auth.strategy
59 | Declares a named strategy using the jwt scheme.
60 |
61 | `server.auth.strategy('my_jwt_strategy', 'jwt', options)`
62 | #### options
63 | - `options` - Config object containing keys to define your jwt authentication and response with the following:
64 | - `keys` - Object or array of objects containing the key method to be used for jwt verification. The keys object can be expressed in many ways. See [keys option examples](#keys-option-examples) for a handful of ways to express this option.
65 | ##### keys
66 |
67 | There are many ways you can do keys and here is an extensive list of all the key options.
68 | ###### HMAC algorithms
69 | You can do HMAC algorithms a couple of different ways. You can do it either like:
70 |
71 | - `keys` - `'some_shared_secret'` - a string that is used for shared secret.
72 |
73 | OR with optional algorithm and key ID header (kid) like:
74 |
75 | - `keys`
76 | - `key` - String that is used for shared secret.
77 | - `algorithms` - Array of accepted [algorithms](#Key-algorithms-supported-by-jwt) (optional).
78 | - `kid` - String representing the key ID header (optional).
79 | ###### Public algorithms
80 | Similar to the HMAC algorithms you can do it like:
81 |
82 | - `key` - Binary data of the public key. Often retrieve via `Fs.readFileSync('public.pem')`.
83 |
84 | OR with optional algorithm and key ID header (kid) like:
85 |
86 | - `keys`
87 | - `key` - Binary data of the public key. Often retrieve via `Fs.readFileSync('public.pem')`.
88 | - `algorithms` - Array of accepted [algorithms](#Key-algorithms-supported-by-jwt) (optional).
89 | - `kid` - String representing the key ID header (optional).
90 | ###### Public and RSA algorithms using JWKS
91 | - `keys`
92 | - `uri` - String that defines your json web key set uri.
93 | - `rejectUnauthorized` - Boolean that determines if TLS flag indicating whether the client should reject a response from a server with invalid certificates. Default is `true`.
94 | - `headers` - Object containing the request headers to send to the uri (optional).
95 | - `algorithms` - Array of accepted [algorithms](#Key-algorithms-supported-by-jwt) (optional).
96 | ###### No algorithms
97 | - `keys`
98 | - `algorithms` - `['none']`
99 | ###### Custom Function
100 | - `keys` - `(param) => { return key; }` - Custom function that derives the key.
101 | ###### Keys Option Examples
102 | ```js
103 | // Single shared secret
104 | {
105 | keys: 'some_shared_secret'
106 | }
107 | ...
108 |
109 | // Single shared secret with algorithms and key ID header
110 | {
111 | keys: {
112 | key: 'some_shared_secret',
113 | algorithms: ['HS256', 'HS512'],
114 | kid: 'someKid'
115 | }
116 | }
117 | ...
118 |
119 | // Multiple shared secret
120 | {
121 | keys: ['some_shared_secret_1', 'shared_secret_2', 'shared_secret_3']
122 | }
123 | ...
124 |
125 | // Multiple shared secret with algorithm and key ID header
126 | {
127 | keys: [
128 | {
129 | key: 'some_shared_secret'
130 | algorithms: ['HS256', 'HS512'],
131 | kid: 'someKid'
132 | },
133 | {
134 | key: 'shared_secret2'
135 | algorithms: ['HS512'],
136 | kid: 'someKid2'
137 | }
138 | ]
139 | }
140 | ...
141 |
142 | // Single Public Key
143 | {
144 | keys: fs.readFileSync('public.pem')
145 | }
146 | ...
147 |
148 | // Single EdDSA key with algorithms
149 | {
150 | keys: {
151 | key: Mock.pair('EdDSA', 'ed25519').public,
152 | algorithms: ['EdDSA']
153 | }
154 | }
155 |
156 | ...
157 | // Single JWKS with headers and algorithms
158 | {
159 | keys: {
160 | uri: 'https://jwks-provider.com/.well-known/jwks.json',
161 | headers: {'x-org-name': 'my_company'},
162 | algorithms: ['RS256', 'RS512']
163 | }
164 | }
165 | ...
166 |
167 | // No algorithms
168 | {
169 | keys: ['none']
170 | }
171 | ...
172 |
173 | // Single custom function
174 | // This function accomplishes the same thing as Single shared secret
175 | {
176 | keys: () => { return 'some_shared_secret'; }
177 | }
178 | ```
179 | ###### Important Security Note
180 |
181 | It is not advisable to put shared secrets in your source code, use environment variables and/or other encryption methods to encrypt/decrypt your shared secret. It is also not advisable to use no algorithms. Both of these practices are ideal for local testing and should be used with caution.
182 | ##### verify
183 |
184 | In addition to keys you can provide other options.
185 | - `verify` - Object to determine how key contents are verified beyond key signature. Set to `false` to do no verification. This includes the `keys` even if they are defined.
186 | - `aud` - String or `RegExp` **or** array of strings or `RegExp` that matches the audience of the token. Set to boolean `false` to not verify aud. Required if `verify` is not `false`.
187 | - `iss` - String or array of strings that matches the issuer of the token. Set to boolean `false` to not verify iss. Required if `verify` is not `false`.
188 | - `sub` - String or array of strings that matches the subject of the token. Set to boolean `false` to not verify sub. Required if `verify` is not `false`.
189 | - `nbf` - Boolean to determine if the "Not Before" [NumericDate](#registered-claim-names) of the token should be validated. Default is `true`.
190 | - `exp` - Boolean to determine if the "Expiration Time" [NumericDate](#registered-claim-names) of the token should be validated. Default is `true`.
191 | - `maxAgeSec` - Integer to determine the maximum age of the token in seconds. Default is `0`. This is time validation using the "Issued At" [NumericDate](#registered-claim-names) (`iat`). Please note that `0` effectively disables this validation, it does not make the maximum age of the token 0 seconds. Also if `maxAgeSec` is not `0` and `exp` is `true`, both will be validated and if either validation fails, the token validation will fail.
192 | - `timeSkewSec` - Integer to adust `exp` and `maxAgeSec` to account for server time drift in seconds. Default is `0`.
193 | ##### headless
194 | - `headless` - String representing `base64` header **or** an Object to use as a header on headless tokens. If this is set, tokens that contain a header section will return `401`.
195 | ##### httpAuthScheme
196 | - `httpAuthScheme` - String the represents the Authentication Scheme. Default is `'Bearer'`.
197 | ##### unauthorizedAttributes
198 | - `unauthorizedAttributes` - String passed directly to `Boom.unauthorized` if no custom err is thrown. Useful for setting realm attribute in WWW-Authenticate header. Defaults to `undefined`.
199 | ##### validate
200 | - `validate` - Function that allows additional validation based on the decoded payload and to put specific credentials in the request object. Can be set to `false` if no additional validation is needed. Setting this to `false` will also set the credentials to be the exact payload of the token, including the [Registered Claim Names](#registered-claim-names).
201 |
202 | The validate function has a signature of `[async] function (artifacts, request, h)` where:
203 | - `artifacts` - An object that contains information from the token.
204 | - `token` - The complete token that was sent.
205 | - `decoded` - An object that contains decoded token.
206 | - `header` - An object that contain the header information.
207 | - `alg` - The algorithm used to sign the token.
208 | - `typ` - The token type (should be `'JWT'` if present) (optional).
209 | - `payload` - An object containing the payload.
210 | - `signature` - The signature string of the token.
211 | - `raw` - An object that contains the token that was sent broken out by `header`, `payload`, and `signature`.
212 | - `keys` - An array of information about key(s) used for authentication
213 | - `key` - The key.
214 | - `algorithm` - The algorithm used to sign the token.
215 | - `kid` - The key ID header. `undefined` if none was set.
216 | - `request` - Is the hapi request object of the request which is being authenticated.
217 | - `h` - The response toolkit.
218 | - Returns an object `{ isValid, credentials, response }` where:
219 | - `isValid` - Boolean that should be set to `true` if additional validation passed, otherwise `false`.
220 | - `credentials` - Object passed back to the application in `request.auth.credentials`.
221 | - `response` - Will be used immediately as a takeover response. `isValid` and `credentials` are ignored if provided.
222 | - Throwing an error from this function will replace default `message` in the `Boom.unauthorized` error.
223 | - Typically, `credentials` are only included when `isValid` is `true`, but there are cases when the application needs to know who tried to authenticate even when it fails (e.g. with authentication mode `'try'`).
224 | ###### validate example
225 | Token payload:
226 | ```js
227 | {
228 | user: 'some_user_name',
229 | group: 'hapi_community'
230 | }
231 | ```
232 | Function:
233 | ```js
234 | validate: (artifacts, request, h) => {
235 |
236 | if (artifacts.decoded.payload.user === 'help') {
237 | return { response: h.redirect('https://hapi.dev/module/jwt/') }; // custom response
238 | }
239 |
240 | if (artifacts.decoded.payload.user === 'crash') {
241 | throw new Error('We hit a tree!'); // custom message in Boom.unauthorized
242 | }
243 |
244 | let isValid;
245 | if (artifacts.decoded.payload.group === 'hapi_community') {
246 | isValid = true;
247 | }
248 | else {
249 | isValid = false;
250 | }
251 |
252 | // Return isValid value based on group
253 | // Set credentials object to have the key username with a value of the user value from the payload
254 |
255 | return {
256 | isValid,
257 | credentials: { username: artifacts.decoded.payload.user }
258 | };
259 | }
260 | ```
261 |
262 | ##### headerName
263 |
264 | - `headerName` - Tells the jwt plugin to read the token from the header specified. Default is `'authorization'`.
265 |
266 | ##### cookieName
267 |
268 | - `cookieName` - Tells the jwt plugin to read the token from the cookie specified. Note that the plugin does not allow you to read from cookie and header at the same time, either read from a header or from a cookie. If you want to read from cookie and header you must use multiple strategies with in which one will have `headerName` config and other will have `cookieName` config. Defaults to `undefined`.
269 |
270 |
271 | ## token
272 |
273 | In addition to creating an auth strategy, the `jwt` module can be used directly even if you aren't using hapi, to run token based functions.
274 | ```js
275 | // Load modules
276 |
277 | const Jwt = require('@hapi/jwt');
278 |
279 | // Generate a Token
280 |
281 | const token = Jwt.token.generate(
282 | {
283 | aud: 'urn:audience:test',
284 | iss: 'urn:issuer:test',
285 | user: 'some_user_name',
286 | group: 'hapi_community'
287 | },
288 | {
289 | key: 'some_shared_secret',
290 | algorithm: 'HS512'
291 | },
292 | {
293 | ttlSec: 14400 // 4 hours
294 | }
295 | );
296 |
297 | // Decode a token
298 |
299 | const decodedToken = Jwt.token.decode(token);
300 |
301 |
302 | // Create function to verify a token
303 |
304 | const verifyToken = (artifact, secret, options = {}) => {
305 |
306 | try {
307 | Jwt.token.verify(artifact, secret, options);
308 | return { isValid: true };
309 | }
310 | catch (err) {
311 | return {
312 | isValid: false,
313 | error: err.message
314 | };
315 | }
316 |
317 | };
318 |
319 | // Get response of a successful verification
320 |
321 | const validResponse = verifyToken(decodedToken, 'some_shared_secret');
322 |
323 | // Get response of a unsuccessful verification due to wrong shared secret
324 |
325 | const badSecretResponse = verifyToken(decodedToken, 'some_unshared_secret');
326 |
327 | // Get response of a unsuccessful verification due to wrong iss
328 |
329 | const badIssResponse = verifyToken(decodedToken, 'some_shared_secret', { iss: 'urn:issuer:different_test' });
330 |
331 | // Display results to console
332 |
333 | console.dir(
334 | {
335 | token,
336 | decodedToken,
337 | validResponse,
338 | badSecretResponse,
339 | badIssResponse
340 | },
341 | { depth: null }
342 | );
343 | ```
344 | Displays the following to the console:
345 | ```js
346 | {
347 | token: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1cm46YXVka...', // Will vary based on time
348 | decodedToken: {
349 | token: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1cm46YXVka...', // Will vary based on time
350 | decoded: {
351 | header: { alg: 'HS512', typ: 'JWT' },
352 | payload: {
353 | aud: 'urn:audience:test',
354 | iss: 'urn:issuer:test',
355 | user: 'some_user_name',
356 | group: 'hapi_community',
357 | iat: 1600604562, // Will vary based on time
358 | exp: 1600618962 // Will vary based on time
359 | },
360 | signature: 'yh3ASEIrgNJZn...' // Will vary based on time
361 | },
362 | raw: {
363 | header: 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9',
364 | payload: 'eyJhdWQiOiJ1cm46YXVka...', // Will vary based on time
365 | signature: 'yh3ASEIrgNJZn...' // Will vary based on time
366 | }
367 | },
368 | validResponse: { isValid: true },
369 | badSecretResponse: { isValid: false, error: 'Invalid token signature' },
370 | badIssResponse: { isValid: false, error: 'Token payload iss value not allowed' }
371 | }
372 | ```
373 | ### generate
374 | `generate(payload, secret, [options])`
375 |
376 | Generates a token as a string where:
377 | - `payload` - Object that contains [Registered Claim Names](#registered-claim-names) (optional) and additional credentials (optional). While both [Registered Claim Names](#registered-claim-names) and additional credentials are optional, an empty payload `{}`, would result in a key that only has an `iat` of now. This would make a token that is valid for one second and containing no other information.
378 | - `secret` - String or buffer that creates signature **or** object where:
379 | - `key` - String or buffer that creates signature.
380 | - `algorithm`- String containing an accepted [algorithm](#Key-algorithms-supported-by-jwt) to be used. Default is `'HS256'`.
381 | - `options` - Optional configuration object with the following:
382 | - `header` - Object to put additional key/value pairs in the header of the token in addition to `alg` and `typ`.
383 | - `typ` Boolean if set to `false` `typ: 'JWT'` is not included in the header.
384 | - `now` - Integer as an alternative way to set `iat` claim. Takes JavaScript style epoch time (with ms). `iat` claim must not be set and `iat` option must not be `false`. Milliseconds are truncated, not rounded.
385 | - `ttlSec` - Integer as an alternative way to set `exp` claim. `exp` is set to be `iat` + `ttlSec`. `exp` claim must not be set.
386 | - `iat` - Boolean if set to `false` to turn off default behavior of creating an `iat` claim.
387 | - `headless` - Boolean if set to `true` will create a headless token. Default is `false`.
388 | ### decode
389 | `decode(token, [options])`
390 |
391 | Returns an Object of a decoded token in the format of `artifacts` described in the [`validate`](#more-on-the-validate-function) section above. This does not verify the token, it only decodes it where:
392 | - `token` - String of encoded token.
393 | - `options` - Optional configuration object with the following:
394 | - `headless`: String representing `base64` header **or** an Object to use as a header on headless tokens. If this is set, tokens that contain a header section will create an error. Default is `null`.
395 | ### verify
396 | `verify(artifacts, secret, [options])`
397 |
398 | A function that will complete if verification passes or throw an error if verification fails where:
399 | - `artifacts` - Object of a decoded token in the format of `artifacts` described in the [`validate`](#more-on-the-validate-function) section above.
400 | - `secret` - String or buffer that creates signature **or** object where:
401 | - `key` - String or buffer that creates signature.
402 | - `algorithm`- String containing an accepted [algorithm](#Key-algorithms-supported-by-jwt) to be used. Default is `'HS256'`.
403 | - `options` - Optional configuration object with the following:
404 | - `aud`- String or `RegExp` **or** array of strings or `RegExp` that matches the audience of the token.
405 | - `iss` - String or array of strings that matches the issuer of the token.
406 | - `sub` - String or array of strings that matches the subject of the token.
407 | - `jti` - String or array of strings that matches the JWT ID of the token.
408 | - `nonce` - String or array of strings that matches the `nonce` of the token. `nonce` is used on [Open ID](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes) for the ID Tokens.
409 | - `nbf` - Integer that represents the "Not Before" [NumericDate](#registered-claim-names) of the token.
410 | - `exp` - Integer that represents the "Expiration Time" [NumericDate](#registered-claim-names) of the token.
411 | - `now` - Integer that represents the current time in JavaScript epoch format (with msecs). When evaluated the msecs are truncated, not rounded.
412 | - `maxAgeSec` - Integer to determine the maximum age of the token in seconds. This is time validation using the "Issued At" [NumericDate](#registered-claim-names) (`iat`).
413 | - `timeSkewSec` - Integer to adust `exp` and `maxAgeSec` to account for server time drift in seconds.
414 | ### verifySignature
415 | `verifySignature({ decoded, raw }, secret)`
416 |
417 | A function that will complete if the signature is valid or throw an error if invalid. This does not do verification on the payload. An expired token will not throw an error if the signature is valid, where:
418 | - `decoded` - Object of decoded token in the format of `artifacts.decoded` described in the [`validate`](#more-on-the-validate-function) section above.
419 | - `raw` - Object of decoded token in the format of `artifacts.raw` described in the [`validate`](#more-on-the-validate-function) section above.
420 | - `secret` - String or buffer that creates signature **or** object where:
421 | - `key` - String or buffer that creates signature.
422 | - `algorithm`- String containing an accepted [algorithm](#Key-algorithms-supported-by-jwt) to be used. Default is `'HS256'`.
423 | ### verifyPayload
424 | `verifyPayload({ decoded }, [options])`
425 |
426 | A function that will complete if payload verification passes or throw an error if payload verification fails. This does not do verification on the signature, where:
427 | - `decoded` - Object of decoded token in the format of `artifacts.decoded` described in the [`validate`](#more-on-the-validate-function) section above.
428 | - `options` - Optional configuration object in format of `options` described in the [`verify`](#verify(artifacts,-secret,-[options])) section above.
429 | ### verifyTime
430 | `verifyTime({ decoded }, [options, nowSec])`
431 |
432 | A function that will complete if `iat` and `exp` verification pass and throw an error if verification fails. This is a subset of `verifyPayload` for only `iat` and `exp` where:
433 | - `decoded` - Object of decoded token in the format of `artifacts.decoded` described in the [`validate`](#more-on-the-validate-function) section above.
434 | - `options` - Optional configuration object with the following:
435 | - `now` - Integer that represents the current time in JavaScript epoch format (with msecs). When evaluated the msecs are truncated, not rounded. Either this or `nowSec` need to be defined.
436 | - `exp` - Integer that represents the "Expiration Time" [NumericDate](#registered-claim-names) of the token.
437 | - `maxAgeSec` - Integer to determine the maximum age of the token in seconds. This is time validation using the "Issued At" [NumericDate](#registered-claim-names) (`iat`).
438 | - `timeSkewSec` - Integer to adust `exp` and `maxAgeSec` to account for server time drift in seconds.
439 | ## Additional Information
440 | ### Registered Claim Names
441 | List and explanation of Registered Claim Names according to [RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1). Please note that `NumericDate` refers to a timestamp in UNIX epoch time, without milliseconds. Whereas Javascript integer timestamps include milliseconds.
442 | - `iss` - The "iss" (issuer) claim identifies the principal that issued the
443 | JWT. Expressed in a string.
444 | - `sub`- The "sub" (subject) claim identifies the principal that is the
445 | subject of the JWT. Expressed in a string.
446 | - `aud` - The "aud" (audience) claim identifies the recipients that the JWT is
447 | intended for. Expressed in a string.
448 | - `exp` - The "exp" (expiration time) claim identifies the expiration time on
449 | or after which the JWT MUST NOT be accepted for processing. Expressed in `NumericDate`.
450 | - `nbf` - The "nbf" (not before) claim identifies the time before which the JWT
451 | MUST NOT be accepted for processing. Expressed in `NumericDate`.
452 | - `iat` - The "iat" (issued at) claim identifies the time at which the JWT was
453 | issued. Expressed in `NumericDate`.
454 | - `jti` - The "jti" (JWT ID) claim provides a unique identifier for the JWT. Expressed in a string.
455 | - `nonce` - While `nonce` is not an [RFC 7519](https://tools.ietf.org/html/rfc7519#section-4.1) Registered Claim, it is used on [Open ID](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes) for the ID Tokens.
456 | ### Key algorithms supported by jwt
457 | - Public: `['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512', 'EdDSA']`
458 | - RSA: `['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']`
459 | - HMAC: `['HS256', 'HS384', 'HS512']`
460 | - No Algorithm: `['none']`
461 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019-2022, Project contributors
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
7 | * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission.
8 |
9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # @hapi/jwt
4 |
5 | #### JWT (JSON Web Token) Authentication.
6 | **jwt** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together.
7 |
8 | ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support
9 |
10 | ## Useful resources
11 |
12 | - [Documentation and API](https://hapi.dev/family/jwt/)
13 | - [Versions status](https://hapi.dev/resources/status/#jwt) (builds, dependencies, node versions, licenses, eol)
14 | - [Changelog](https://hapi.dev/family/jwt/changelog/)
15 | - [Project policies](https://hapi.dev/policies/)
16 |
17 | ## Acknowledgements
18 |
19 | Portions of this module were adapted from:
20 |
21 | - [node-jwa](https://github.com/brianloveswords/node-jwa), copyright (c) 2013 Brian J. Brennan, MIT License
22 | - [node-jws](https://github.com/brianloveswords/node-jws), copyright (c) 2013 Brian J. Brennan, MIT License
23 | - [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken), copyright (c) 2015 Auth0, Inc., MIT License
24 | - [node-jwks-rsa](https://github.com/auth0/node-jwks-rsa), copyright (c) 2016 Sandrino Di Mattia, MIT License
25 | - [hapi-auth-jwt2](https://github.com/dwyl/hapi-auth-jwt2), copyright (c) 2015-2016, dwyl ltd, ISC License
26 | - [mock-jwks](https://github.com/Levino/mock-jwks), copyright (c) 2018-2019 Levin Keller, MIT License
27 | - [Stack Overflow answer](http://stackoverflow.com/questions/18835132/xml-to-pem-in-node-js)
28 | - [node-rsa-pem-from-mod-exp](https://github.com/tracker1/node-rsa-pem-from-mod-exp), copyright (c) 2014 Michael J. Ryan, MIT License
29 |
--------------------------------------------------------------------------------
/lib/crypto.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Crypto = require('crypto');
4 |
5 | const Cryptiles = require('@hapi/cryptiles');
6 | const EcdsaSigFormatter = require('ecdsa-sig-formatter');
7 |
8 | const Utils = require('./utils');
9 |
10 |
11 | const internals = {
12 | algorithms: {
13 | RS256: 'RSA-SHA256',
14 | RS384: 'RSA-SHA384',
15 | RS512: 'RSA-SHA512',
16 |
17 | PS256: 'RSA-SHA256',
18 | PS384: 'RSA-SHA384',
19 | PS512: 'RSA-SHA512',
20 |
21 | ES256: 'RSA-SHA256',
22 | ES384: 'RSA-SHA384',
23 | ES512: 'RSA-SHA512',
24 |
25 | EDDSA: 'EdDSA',
26 |
27 | HS256: 'sha256',
28 | HS384: 'sha384',
29 | HS512: 'sha512'
30 | }
31 | };
32 |
33 |
34 | exports.generate = function (value, algorithm, key) {
35 |
36 | algorithm = algorithm.toUpperCase();
37 |
38 | if (algorithm === 'NONE') {
39 | return '';
40 | }
41 |
42 | const algo = internals.algorithms[algorithm];
43 | if (!algo) {
44 | throw new Error('Unsupported algorithm');
45 | }
46 |
47 | switch (algorithm) {
48 | case 'RS256':
49 | case 'RS384':
50 | case 'RS512': {
51 |
52 | const signer = Crypto.createSign(algo);
53 | signer.update(value);
54 | const sig = signer.sign(key, 'base64');
55 | return internals.b64urlEncode(sig);
56 | }
57 |
58 | case 'PS256':
59 | case 'PS384':
60 | case 'PS512': {
61 |
62 | const signer = Crypto.createSign(algo);
63 | signer.update(value);
64 | const sig = signer.sign({ key, padding: Crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: Crypto.constants.RSA_PSS_SALTLEN_DIGEST }, 'base64');
65 | return internals.b64urlEncode(sig);
66 | }
67 |
68 | case 'ES256':
69 | case 'ES384':
70 | case 'ES512': {
71 |
72 | const signer = Crypto.createSign(algo);
73 | signer.update(value);
74 | const sig = signer.sign(key, 'base64');
75 | return EcdsaSigFormatter.derToJose(sig, algorithm);
76 | }
77 |
78 | case 'HS256':
79 | case 'HS384':
80 | case 'HS512': {
81 |
82 | const hmac = Crypto.createHmac(algo, key);
83 | hmac.update(value);
84 | const digest = hmac.digest('base64');
85 | return internals.b64urlEncode(digest);
86 | }
87 |
88 | case 'EDDSA': {
89 |
90 | const sig = Crypto.sign(undefined, Buffer.from(value), key);
91 | return internals.b64urlEncode(sig.toString('base64'));
92 | }
93 | }
94 | };
95 |
96 |
97 | exports.verify = function (raw, algorithm, key) {
98 |
99 | algorithm = algorithm.toUpperCase();
100 |
101 | if (algorithm === 'NONE') {
102 | return raw.signature === '';
103 | }
104 |
105 | const algo = internals.algorithms[algorithm];
106 | if (!algo) {
107 | throw new Error('Unsupported algorithm');
108 | }
109 |
110 | const value = `${raw.header}.${raw.payload}`;
111 | const signature = raw.signature;
112 |
113 | switch (algorithm) {
114 | case 'RS256':
115 | case 'RS384':
116 | case 'RS512': {
117 |
118 | const verifier = Crypto.createVerify(algo);
119 | verifier.update(value);
120 | return verifier.verify(key, internals.b64urlDecode(signature), 'base64');
121 | }
122 |
123 | case 'PS256':
124 | case 'PS384':
125 | case 'PS512': {
126 |
127 | const verifier = Crypto.createVerify(algo);
128 | verifier.update(value);
129 | return verifier.verify({ key, padding: Crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: Crypto.constants.RSA_PSS_SALTLEN_DIGEST }, internals.b64urlDecode(signature), 'base64');
130 | }
131 |
132 | case 'ES256':
133 | case 'ES384':
134 | case 'ES512': {
135 |
136 | const sig = EcdsaSigFormatter.joseToDer(signature, algorithm).toString('base64');
137 | const verifier = Crypto.createVerify(algo);
138 | verifier.update(value);
139 | return verifier.verify(key, internals.b64urlDecode(sig), 'base64');
140 | }
141 |
142 | case 'HS256':
143 | case 'HS384':
144 | case 'HS512': {
145 |
146 | const compare = exports.generate(value, algorithm, key);
147 | return Cryptiles.fixedTimeComparison(signature, compare);
148 | }
149 |
150 | case 'EDDSA': {
151 |
152 | const sig = Buffer.from(internals.b64urlDecode(signature), 'base64');
153 | return Crypto.verify(undefined, Buffer.from(value), key, sig);
154 | }
155 | }
156 | };
157 |
158 |
159 | internals.b64urlEncode = function (b64) {
160 |
161 | return b64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
162 | };
163 |
164 |
165 | internals.b64urlDecode = function (b64url) {
166 |
167 | b64url = b64url.toString();
168 |
169 | const padding = 4 - b64url.length % 4;
170 | if (padding !== 4) {
171 | for (let i = 0; i < padding; ++i) {
172 | b64url += '=';
173 | }
174 | }
175 |
176 | return b64url.replace(/\-/g, '+').replace(/_/g, '/');
177 | };
178 |
179 |
180 | exports.certToPEM = function (cert) {
181 |
182 | return `-----BEGIN CERTIFICATE-----\n${internals.chop(cert)}\n-----END CERTIFICATE-----\n`;
183 | };
184 |
185 |
186 | exports.rsaPublicKeyToPEM = function (modulusB64, exponentB64) {
187 |
188 | const modulusHex = internals.prepadSigned(Buffer.from(modulusB64, 'base64').toString('hex'));
189 | const exponentHex = internals.prepadSigned(Buffer.from(exponentB64, 'base64').toString('hex'));
190 |
191 | const modlen = modulusHex.length / 2;
192 | const explen = exponentHex.length / 2;
193 |
194 | const encodedModlen = internals.encodeLengthHex(modlen);
195 | const encodedExplen = internals.encodeLengthHex(explen);
196 |
197 | const encodedPubkey = '30' +
198 | internals.encodeLengthHex(modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2) +
199 | '02' + encodedModlen + modulusHex +
200 | '02' + encodedExplen + exponentHex;
201 |
202 | const der = internals.chop(Buffer.from(encodedPubkey, 'hex').toString('base64'));
203 | return `-----BEGIN RSA PUBLIC KEY-----\n${der}\n-----END RSA PUBLIC KEY-----\n`;
204 | };
205 |
206 |
207 | internals.prepadSigned = function (hexStr) {
208 |
209 | const msb = hexStr[0];
210 | if (msb > '7') {
211 | return `00${hexStr}`;
212 | }
213 |
214 | return hexStr;
215 | };
216 |
217 |
218 | internals.encodeLengthHex = function (n) {
219 |
220 | if (n <= 127) {
221 | return Utils.toHex(n);
222 | }
223 |
224 | const nHex = Utils.toHex(n);
225 | const lengthOfLengthByte = 128 + nHex.length / 2;
226 | return Utils.toHex(lengthOfLengthByte) + nHex;
227 | };
228 |
229 |
230 | internals.chop = function (cert) {
231 |
232 | return cert.match(/.{1,64}/g).join('\n');
233 | };
234 |
--------------------------------------------------------------------------------
/lib/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for @hapi/jwt 2.0
2 | // Project: https://github.com/hapijs/jwt
3 | // Definitions by: Sergio Sánchez , Danilo Alonso
4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
5 | // TypeScript Version: 4.2
6 |
7 | import { Plugin, ResponseToolkit, Request, ResponseObject, ReqRef, ReqRefDefaults, MergeRefs } from '@hapi/hapi';
8 |
9 | declare module '@hapi/hapi' {
10 | interface ServerAuth {
11 | /**
12 | * Function to define the server authentication strategy to be used.
13 | *
14 | * @param name string name to define the strategy.
15 | * @param scheme jwt for this plugin.
16 | * @param options jwt plugin options.
17 | */
18 | strategy(name: string, scheme: 'jwt', options?: HapiJwt.Options): void;
19 | }
20 | }
21 |
22 | export declare namespace HapiJwt {
23 | // Common definitions
24 |
25 | type SupportedAlgorithm =
26 | | 'RS256'
27 | | 'RS384'
28 | | 'RS512'
29 | | 'PS256'
30 | | 'PS384'
31 | | 'PS512'
32 | | 'ES256'
33 | | 'ES384'
34 | | 'ES512'
35 | | 'HS256'
36 | | 'HS384'
37 | | 'HS512'
38 | | 'EdDSA';
39 | type NoAlgorithm = 'none';
40 |
41 | interface StandardKey {
42 | /**
43 | * String or binary data that is used for shared secret.
44 | */
45 | key: string | Buffer;
46 | /**
47 | * Array of accepted algorithms
48 | */
49 | algorithms?: SupportedAlgorithm[] | undefined;
50 | /**
51 | * String representing the key ID header.
52 | */
53 | kid?: string | undefined;
54 | }
55 |
56 | interface JWKSKey {
57 | /**
58 | * String that defines your json web key set uri.
59 | */
60 | uri: string;
61 | /**
62 | * Boolean that determines if TLS flag indicating whether the client should reject a response from a server with invalid certificates. Default is true.
63 | */
64 | rejectUnauthorized?: boolean | undefined;
65 | /**
66 | * Object containing the request headers to send to the uri.
67 | */
68 | header?: object | undefined;
69 | /**
70 | * Array of accepted algorithms.
71 | */
72 | algorithms?: SupportedAlgorithm[] | undefined;
73 | }
74 |
75 | type Key = StandardKey | JWKSKey;
76 |
77 | interface JwtRefs {
78 | JwtPayload?: any
79 | }
80 |
81 | interface DecodedToken {
82 | /**
83 | * An object that contain the header information.
84 | */
85 | header: {
86 | /**
87 | * The algorithm used to sign the token.
88 | */
89 | alg: string;
90 | /**
91 | * The token type.
92 | */
93 | typ?: 'JWT' | undefined;
94 | };
95 | /**
96 | * An object containing the payload.
97 | */
98 | payload: Refs['JwtPayload'];
99 | /**
100 | * The signature string of the token.
101 | */
102 | signature: string;
103 | }
104 |
105 | interface RawToken {
106 | /**
107 | * The header of the token.
108 | */
109 | header: string;
110 | /**
111 | * The payload of the token.
112 | */
113 | payload: string;
114 | /**
115 | * The signature of the token.
116 | */
117 | signature: string;
118 | }
119 |
120 |
121 | interface Artifacts {
122 | /**
123 | * The complete token that was sent.
124 | */
125 | token: string;
126 | /**
127 | * An object that contains decoded token.
128 | */
129 | decoded: DecodedToken;
130 | /**
131 | * An object that contains the token that was sent broken out by header, payload, and signature.
132 | */
133 | raw: RawToken;
134 | /**
135 | * An array of information about key(s) used for authentication.
136 | */
137 | keys?: StandardKey[] | undefined;
138 | }
139 |
140 | // Plugin definitions
141 |
142 | interface VerifyKeyOptions {
143 | /**
144 | * String or RegExp or array of strings or RegExp that matches the audience of the token. Set to boolean false to not verify aud.
145 | */
146 | aud: string | string[] | RegExp | RegExp[] | false;
147 | /**
148 | * String or array of strings that matches the issuer of the token. Set to boolean false to not verify iss.
149 | */
150 | iss: string | string[] | false;
151 | /**
152 | * String or array of strings that matches the subject of the token. Set to boolean false to not verify sub.
153 | */
154 | sub: string | string[] | false;
155 | /**
156 | * Boolean to determine if the "Not Before" NumericDate of the token should be validated. Default is true.
157 | */
158 | nbf?: boolean | undefined;
159 | /**
160 | * Boolean to determine if the "Expiration Time" NumericDate of the token should be validated. Default is true.
161 | */
162 | exp?: boolean | undefined;
163 | /**
164 | * Integer to determine the maximum age of the token in seconds. Default is 0.
165 | */
166 | maxAgeSec?: number | undefined;
167 | /**
168 | * Integer to adust exp and maxAgeSec to account for server time drift in seconds. Default is 0.
169 | */
170 | timeSkewSec?: number | undefined;
171 | }
172 |
173 | interface ValidationResult {
174 | /**
175 | * Boolean that should be set to true if additional validation passed, otherwise false.
176 | */
177 | isValid: boolean;
178 | /**
179 | * Object passed back to the application in request.auth.credentials.
180 | */
181 | credentials?: MergeRefs['AuthUser'] | undefined;
182 | /**
183 | * Will be used immediately as a takeover response. isValid and credentials are ignored if provided.
184 | */
185 | response?: ResponseObject | Error | undefined;
186 | }
187 |
188 | interface OptionsValidateFunction {
189 | (
190 | artifacts: Artifacts,
191 | request: Request,
192 | h: ResponseToolkit
193 | ): Promise | never
194 | }
195 |
196 | interface Options {
197 | /**
198 | * The key method to be used for jwt verification.
199 | */
200 | keys: string | string[] | Buffer | Key | Key[] | NoAlgorithm[] | ((param: any) => string);
201 | /**
202 | * Object to determine how key contents are verified beyond key signature. Set to false to do no verification.
203 | */
204 | verify: VerifyKeyOptions | false;
205 | /**
206 | * String the represents the Authentication Scheme. Default is 'Bearer'.
207 | */
208 | httpAuthScheme?: string | undefined;
209 | /**
210 | * String passed directly to Boom.unauthorized if no custom err is thrown. Defaults to undefined.
211 | */
212 | unauthorizedAttributes?: string | undefined;
213 | /**
214 | * Function that allows additional validation based on the decoded payload and to put specific credentials in the request object. Can be set to false if no additional validation is needed.
215 | *
216 | * @param artifacts an object that contains information from the token.
217 | * @param request the hapi request object of the request which is being authenticated.
218 | * @param h the response toolkit.
219 | */
220 | validate: OptionsValidateFunction | false;
221 | }
222 |
223 | // Token definitions
224 |
225 | type AdditionalCredentials = any;
226 |
227 | interface Payload extends AdditionalCredentials {
228 | /**
229 | * The "iss" (issuer) claim identifies the principal that issued the JWT. Expressed in a string.
230 | */
231 | iss?: string | undefined;
232 | /**
233 | * The "sub" (subject) claim identifies the principal that is the subject of the JWT. Expressed in a string.
234 | */
235 | sub?: string | undefined;
236 | /**
237 | * The "aud" (audience) claim identifies the recipients that the JWT is intended for. Expressed in a string.
238 | */
239 | aud?: string | undefined;
240 | /**
241 | * The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. Expressed in NumericDate.
242 | */
243 | exp?: number | undefined;
244 | /**
245 | * The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. Expressed in NumericDate.
246 | */
247 | nbf?: number | undefined;
248 | /**
249 | * The "iat" (issued at) claim identifies the time at which the JWT was issued. Expressed in NumericDate.
250 | */
251 | iat?: number | undefined;
252 | /**
253 | * The "jti" (JWT ID) claim provides a unique identifier for the JWT. Expressed in a string.
254 | */
255 | jti?: string | undefined;
256 | /**
257 | * While nonce is not an RFC 7519 Registered Claim, it is used on Open ID for the ID Tokens.
258 | */
259 | nonce?: string | undefined;
260 | }
261 |
262 | type Secret = string | Buffer | { key: string | Buffer; algorithm: SupportedAlgorithm | NoAlgorithm };
263 |
264 | interface GenerateOptions {
265 | /**
266 | * Object to put additional key/value pairs in the header of the token in addition to alg and typ.
267 | */
268 | header?: object | undefined;
269 | /**
270 | * Boolean if set to false typ: 'JWT' is not included in the header.
271 | */
272 | typ?: boolean | undefined;
273 | /**
274 | * Integer as an alternative way to set iat claim. Takes JavaScript style epoch time (with ms). iat claim must not be set and iat option must not be false. Milliseconds are truncated.
275 | */
276 | now?: number | undefined;
277 | /**
278 | * Integer as an alternative way to set exp claim. exp is set to be iat + ttlSec. exp claim must not be set.
279 | */
280 | ttlSec?: number | undefined;
281 | /**
282 | * Boolean if set to false typ: 'JWT' is not included in the header.
283 | */
284 | iat?: boolean | undefined;
285 | /**
286 | * String to set the encoding use for stringify the payload. Default is utf8.
287 | */
288 | encoding?: string | undefined;
289 | /**
290 | * Boolean if set to true will decode a valid headless token. Default is false.
291 | */
292 | headless?: boolean | undefined;
293 | }
294 |
295 | interface DecodeOptions {
296 | /**
297 | * Boolean if set to true will decode a valid headless token. Default is false.
298 | */
299 | headless: boolean;
300 | }
301 |
302 | interface VerifyTokenOptions extends TimeOptions {
303 | /**
304 | * String or RegExp or array of strings or RegExp that matches the audience of the token. Set to boolean false to not verify aud.
305 | */
306 | aud: string | string[] | RegExp | RegExp[] | false;
307 | /**
308 | * String or array of strings that matches the issuer of the token. Set to boolean false to not verify iss.
309 | */
310 | iss: string | string[] | false;
311 | /**
312 | * String or array of strings that matches the subject of the token. Set to boolean false to not verify sub.
313 | */
314 | sub: string | string[] | false;
315 | /**
316 | * String or array of strings that matches the JWT ID of the token.
317 | */
318 | jti?: string | string[] | undefined;
319 | /**
320 | * String or array of strings that matches the nonce of the token. nonce is used on Open ID for the ID Tokens.
321 | */
322 | nonce?: string | string[] | undefined;
323 | /**
324 | * Integer that represents the "Not Before" NumericDate of the token.
325 | */
326 | nbf?: number | undefined;
327 | }
328 |
329 | interface TimeOptions {
330 | /**
331 | * Integer that represents the current time in JavaScript epoch format (with msecs). When evaluated the msecs are truncated, not rounded. Either this or nowSec need to be defined.
332 | */
333 | now?: number | undefined;
334 | /**
335 | * Integer that represents the "Expiration Time" NumericDate of the token.
336 | */
337 | exp?: number | undefined;
338 | /**
339 | * Integer to determine the maximum age of the token in seconds. This is time validation using the "Issued At" NumericDate (iat).
340 | */
341 | maxAgeSec?: number | undefined;
342 | /**
343 | * Integer to adjust exp and maxAgeSec to account for server time drift in seconds.
344 | */
345 | timeSkewSec?: number | undefined;
346 | }
347 |
348 | interface Token {
349 | /**
350 | * Generates a token as a string.
351 | *
352 | * @param payload object of decoded token in artifacts format.
353 | * @param secret object, string or buffer that creates signature.
354 | * @param options optional configuration object.
355 | */
356 | generate: (payload: Payload, secret: Secret, options?: GenerateOptions) => string;
357 | /**
358 | * Returns an object of a decoded token in the format of artifacts. This does not verify the token, it only decodes it.
359 | *
360 | * @param token string of encoded token.
361 | * @param options optional configuration object.
362 | */
363 | decode: (token: string, options?: DecodeOptions) => Artifacts | never;
364 | /**
365 | * A function that will complete if verification passes or throw an error if verification fails.
366 | *
367 | * @param artifacts object of decoded token in artifacts format.
368 | * @param secret object, string or buffer that creates signature.
369 | * @param options optional configuration object.
370 | */
371 | verify: (artifacts: Artifacts, secret: Secret, options?: VerifyTokenOptions) => void | never;
372 | /**
373 | * A function that will complete if the signature is valid or throw an error if invalid. This does not do verification on the payload.
374 | * An expired token will not throw an error if the signature is valid.
375 | *
376 | * @param artifacts object of decoded token in artifacts format.
377 | * @param raw object of decoded token in raw format.
378 | * @param secret object, string or buffer that creates signature.
379 | */
380 | verifySignature: (artifacts: Artifacts, secret: Secret) => void | never;
381 | /**
382 | * A function that will complete if payload verification passes or throw an error if payload verification fails. This does not do verification on the signature.
383 | *
384 | * @param artifacts object of decoded token in artifacts format..
385 | * @param options optional configuration object.
386 | */
387 | verifyPayload: (artifacts: Artifacts, options?: VerifyTokenOptions) => void | never;
388 | /**
389 | * A function that will complete if iat and exp verification pass and throw an error if verification fails. This is a subset of verifyPayload for only iat and exp.
390 | *
391 | * @param artifacts object of decoded token in artifacts format.
392 | * @param options optional configuration object.
393 | * @param nowSec integer that represents the current time in JavaScript epoch format (with msecs). When evaluated the msecs are truncated, not rounded.
394 | */
395 | verifyTime: (artifacts: Artifacts, options?: TimeOptions, nowSec?: number) => void | never;
396 |
397 | signature: {
398 | /**
399 | * Function to generate a signature using a supported algorithm.
400 | *
401 | * @param value string that represents the signer.
402 | * @param algorithm string containing an accepted algorithm to be used.
403 | * @param key string that represents the signature.
404 | */
405 | generate: (value: string, algorithm: SupportedAlgorithm | NoAlgorithm, key: string) => string | never;
406 | /**
407 | * Function to verify a signature using a supported algorithm.
408 | *
409 | * @param raw an object that contains the token that was sent broken out by header, payload, and signature.
410 | * @param algorithm string containing an accepted algorithm to be used.
411 | * @param key string signature to be verify.
412 | */
413 | verify: (raw: RawToken, algorithm: SupportedAlgorithm | NoAlgorithm, key: string) => boolean | never;
414 | };
415 | }
416 |
417 | // Crypto definitions
418 |
419 | interface Crypto {
420 | /**
421 | * Function to convert RSA public key to PEM format.
422 | *
423 | * @param modulusB64 string that represents the modulus (product of two large primes) in base64.
424 | * @param exponentB64 string that represents the encryption exponent in base64.
425 | */
426 | rsaPublicKeyToPEM: (modulusB64: string, exponentB64: string) => string;
427 | }
428 |
429 | // Utils definitions
430 |
431 | interface Utils {
432 | /**
433 | * Function that converts an object to a string in base64.
434 | *
435 | * @param obj object to be converted.
436 | */
437 | b64stringify: (obj: object) => string;
438 | /**
439 | * Function that converts a number to hexadecimal string.
440 | *
441 | * @param number number to be converted.
442 | */
443 | toHex: (number: number) => string;
444 | }
445 | }
446 |
447 |
448 | export declare const plugin: Plugin;
449 | export declare const token: HapiJwt.Token;
450 | export declare const crypto: HapiJwt.Crypto;
451 | export declare const utils: HapiJwt.Utils;
452 |
453 | declare const mod: {
454 | plugin: Plugin;
455 | token: HapiJwt.Token;
456 | crypto: HapiJwt.Crypto;
457 | utils: HapiJwt.Utils;
458 | };
459 |
460 | export default mod;
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Crypto = require('./crypto');
4 | const Plugin = require('./plugin');
5 | const Token = require('./token');
6 | const Utils = require('./utils');
7 |
8 |
9 | exports.plugin = Plugin.plugin;
10 |
11 | exports.token = {
12 | generate: Token.generate,
13 | decode: Token.decode,
14 | verify: Token.verify,
15 |
16 | verifySignature: Token.verifySignature,
17 | verifyPayload: Token.verifyPayload,
18 | verifyTime: Token.verifyTime,
19 |
20 | signature: {
21 | generate: Crypto.generate,
22 | verify: Crypto.verify
23 | }
24 | };
25 |
26 | exports.crypto = {
27 | rsaPublicKeyToPEM: Crypto.rsaPublicKeyToPEM
28 | };
29 |
30 | exports.utils = Utils;
31 |
--------------------------------------------------------------------------------
/lib/keys.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const Wreck = require('@hapi/wreck');
5 |
6 | const Crypto = require('./crypto');
7 |
8 |
9 | const internals = {
10 | keyAlgo: {
11 | none: ['none'],
12 | public: ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512', 'EdDSA'],
13 | rsa: ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'],
14 | hmac: ['HS256', 'HS384', 'HS512']
15 | },
16 | certRx: {
17 | public: /^[\s\-]*BEGIN (?:CERTIFICATE)|(?:PUBLIC KEY)/,
18 | rsa: /^[\s\-]*BEGIN RSA (?:PRIVATE)|(?:PUBLIC)/
19 | }
20 | };
21 |
22 |
23 | internals.supportedAlgorithms = internals.keyAlgo.public.concat(internals.keyAlgo.hmac);
24 |
25 |
26 | module.exports = internals.Provider = class {
27 |
28 | constructor(server, options) {
29 |
30 | this._server = server;
31 | this._settings = options;
32 | this._cache = null;
33 |
34 | // Split sources
35 |
36 | this._statics = [];
37 | this._dynamics = [];
38 | this._remotes = new Map();
39 |
40 | for (const key of options.keys) {
41 | if (Buffer.isBuffer(key) ||
42 | typeof key === 'string') {
43 |
44 | this._statics.push({ key, algorithms: internals.Provider.keyAlgorithms(key) });
45 | }
46 | else if (typeof key === 'function') {
47 | this._dynamics.push(key);
48 | }
49 | else if (key.key !== undefined) {
50 | this._statics.push({ key: key.key, algorithms: key.algorithms ?? internals.Provider.keyAlgorithms(key.key), kid: key.kid });
51 | }
52 | else {
53 | this._remotes.set(key.uri, { algorithms: key.algorithms, wreck: { json: 'force', headers: key.headers, rejectUnauthorized: key.rejectUnauthorized } });
54 | }
55 | }
56 |
57 | // Register provider
58 |
59 | this.hasJwks = !!this._remotes.size;
60 | this._server.plugins.jwt._providers.push(this);
61 | }
62 |
63 | initialize(segment) {
64 |
65 | if (!this.hasJwks) {
66 | return;
67 | }
68 |
69 | const cache = Object.assign({}, this._settings.cache);
70 | cache.segment = segment;
71 | cache.cache = this._server.plugins.jwt._cacheName;
72 | cache.generateFunc = internals.jwks(this);
73 | this._cache = this._server.cache(cache);
74 |
75 | // Warmup cache
76 |
77 | const pending = [];
78 | for (const uri of this._remotes.keys()) {
79 | pending.push(this._cache.get(uri));
80 | }
81 |
82 | return Promise.all(pending);
83 | }
84 |
85 | async assign(artifacts, request) {
86 |
87 | const errors = [];
88 | const keys = [];
89 |
90 | // Add static keys
91 |
92 | internals.append(keys, this._statics, artifacts.decoded.header);
93 |
94 | // Add matching remote keys
95 |
96 | const kid = artifacts.decoded.header.kid;
97 | if (kid &&
98 | this._remotes.size) {
99 |
100 | if (!this._cache) {
101 | throw Boom.internal('Server is not initialized');
102 | }
103 |
104 | for (const uri of this._remotes.keys()) {
105 | try {
106 | const map = await this._cache.get(uri);
107 | internals.append(keys, map.get(kid), artifacts.decoded.header);
108 | }
109 | catch (err) {
110 | errors.push(err);
111 | }
112 | }
113 | }
114 |
115 | // Generate dynamic keys
116 |
117 | for (const method of this._dynamics) {
118 | try {
119 | internals.append(keys, await method(artifacts, request), artifacts.decoded.header);
120 | }
121 | catch (err) {
122 | errors.push(err);
123 | }
124 | }
125 |
126 | if (!keys.length) {
127 | if (errors.length) {
128 | throw Boom.internal('Failed to obtain keys', errors);
129 | }
130 |
131 | return;
132 | }
133 |
134 | if (errors.length) {
135 | artifacts.errors = errors;
136 | }
137 |
138 | artifacts.keys = keys;
139 | }
140 |
141 | static get supportedAlgorithms() {
142 |
143 | return internals.supportedAlgorithms;
144 | }
145 |
146 | static keyAlgorithms(key) {
147 |
148 | if (!key) {
149 | return internals.keyAlgo.none;
150 | }
151 |
152 | const keyString = key.toString();
153 |
154 | if ((typeof key === 'object' && key.asymmetricKeyType) || internals.certRx.public.test(keyString)) {
155 | return internals.keyAlgo.public;
156 | }
157 |
158 | if (internals.certRx.rsa.test(keyString)) {
159 | return internals.keyAlgo.rsa;
160 | }
161 |
162 | return internals.keyAlgo.hmac;
163 | }
164 | };
165 |
166 |
167 | internals.append = function (to, from, { alg, kid }) {
168 |
169 | if (!from) {
170 | return;
171 | }
172 |
173 | const values = Array.isArray(from) ? from : [from];
174 | for (const value of values) {
175 | const key = internals.normalize(value);
176 | if (key.algorithms.includes(alg) &&
177 | (!kid || !key.kid || kid === key.kid)) {
178 |
179 | to.push({ key: key.key, algorithm: alg, kid: key.kid });
180 | }
181 | }
182 | };
183 |
184 |
185 | internals.normalize = function (key) {
186 |
187 | if (typeof key === 'string' ||
188 | Buffer.isBuffer(key)) {
189 |
190 | return { key, algorithms: internals.Provider.keyAlgorithms(key) };
191 | }
192 |
193 | return key;
194 | };
195 |
196 |
197 | internals.jwks = function (provider) {
198 |
199 | return async function (uri) {
200 |
201 | const remote = provider._remotes.get(uri);
202 |
203 | try {
204 | var { payload } = await Wreck.get(uri, remote.wreck);
205 | }
206 | catch (err) {
207 | throw Boom.internal('JWKS endpoint error', err);
208 | }
209 |
210 | if (!payload) {
211 | throw Boom.internal('JWKS endpoint returned empty payload', { uri });
212 | }
213 |
214 | const source = payload.keys;
215 | if (!source ||
216 | !Array.isArray(source) ||
217 | !source.length) {
218 |
219 | throw Boom.internal('JWKS endpoint returned invalid payload', { uri, payload });
220 | }
221 |
222 | const keys = new Map();
223 | for (const key of source) {
224 | if (key.use !== 'sig' ||
225 | key.kty !== 'RSA' ||
226 | !key.kid) {
227 |
228 | continue;
229 | }
230 |
231 | if (key.x5c?.length) {
232 |
233 | const algorithms = internals.algorithms(key, remote, 'public');
234 | if (algorithms) {
235 | keys.set(key.kid, { key: Crypto.certToPEM(key.x5c[0]), algorithms });
236 | }
237 | }
238 | else if (key.n && key.e) {
239 |
240 | const algorithms = internals.algorithms(key, remote, 'rsa');
241 | if (algorithms) {
242 | keys.set(key.kid, { key: Crypto.rsaPublicKeyToPEM(key.n, key.e), algorithms });
243 | }
244 | }
245 | }
246 |
247 | if (!keys.size) {
248 | throw Boom.internal('JWKS endpoint response contained no valid keys', { uri, payload });
249 | }
250 |
251 | return keys;
252 | };
253 | };
254 |
255 |
256 | internals.algorithms = function (key, remote, type) {
257 |
258 | if (key.alg) {
259 | if (!remote.algorithms ||
260 | remote.algorithms.includes(key.alg)) {
261 |
262 | return [key.alg];
263 | }
264 |
265 | return null;
266 | }
267 |
268 | if (remote.algorithms) {
269 | return remote.algorithms;
270 | }
271 |
272 | return internals.keyAlgo[type];
273 | };
274 |
--------------------------------------------------------------------------------
/lib/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Boom = require('@hapi/boom');
4 | const CatboxObject = require('@hapi/catbox-object');
5 | const Hoek = require('@hapi/hoek');
6 | const Joi = require('joi');
7 |
8 | const Crypto = require('./crypto');
9 | const Keys = require('./keys');
10 | const Token = require('./token');
11 | const Utils = require('./utils');
12 |
13 |
14 | const internals = {};
15 |
16 |
17 | exports.plugin = {
18 | pkg: require('../package.json'),
19 | requirements: {
20 | hapi: '>=20.0.0'
21 | },
22 | register: function (server) {
23 |
24 | server.expose('_providers', []);
25 | server.expose('_caching', false);
26 | server.expose('_cacheName', '@hapi/jwt');
27 |
28 | server.ext('onPreStart', internals.onPreStart);
29 |
30 | server.auth.scheme('jwt', internals.implementation);
31 | }
32 | };
33 |
34 |
35 | internals.onPreStart = function (server) {
36 |
37 | const providers = server.plugins.jwt._providers;
38 |
39 | const pendings = [];
40 | for (let i = 0; i < providers.length; ++i) {
41 | const provider = providers[i];
42 | pendings.push(provider.initialize(`s${i}`));
43 | }
44 |
45 | return Promise.all(pendings);
46 | };
47 |
48 |
49 | internals.schema = {
50 | algorithms: Joi.array()
51 | .items(Joi.string().valid(...Keys.supportedAlgorithms))
52 | .min(1)
53 | .single()
54 | };
55 |
56 |
57 | internals.schema.strategy = Joi.object({
58 |
59 | cache: Joi.object({
60 | segment: Joi.forbidden(),
61 | generateFunc: Joi.forbidden(),
62 | cache: Joi.forbidden(),
63 | shared: Joi.forbidden()
64 | })
65 | .unknown()
66 | .default({
67 | expiresIn: 7 * 24 * 60 * 60 * 1000, // 1 weeks
68 | staleIn: 60 * 60 * 1000, // 1 hour
69 | staleTimeout: 500, // 500 milliseconds
70 | generateTimeout: 2 * 60 * 1000 // 2 minutes
71 | }),
72 |
73 | cookieName: Utils.validHttpTokenSchema
74 | .optional()
75 | .messages({
76 | 'string.pattern.base':
77 | 'Cookie name cannot start or end with special characters. Valid characters in cookie name are _, -, numbers and alphabets'
78 | }),
79 |
80 | headerName: Joi.any().when('cookieName', {
81 | is: Joi.exist(),
82 | then: Joi.string().forbidden().messages({ 'any.unknown': 'headerName not allowed when cookieName is specified' }),
83 | otherwise: Utils.validHttpTokenSchema.optional()
84 | .default('authorization')
85 | .messages({
86 | 'string.pattern.base': 'Header name must be a valid header name following https://tools.ietf.org/html/rfc7230#section-3.2.6'
87 | })
88 | }),
89 |
90 | headless: [Joi.string(), Joi.object({ alg: Joi.string().valid(...Keys.supportedAlgorithms).required(), typ: Joi.valid('JWT') }).unknown()],
91 |
92 | httpAuthScheme: Joi.string().default('Bearer'),
93 |
94 | keys: Joi.array().
95 | items(
96 | Joi.string(),
97 | Joi.binary(),
98 | Joi.func(),
99 | {
100 | key: Joi.valid('').default(''),
101 | algorithms: Joi.array().items(Joi.valid('none')).length(1).single().required(),
102 | kid: Joi.string()
103 | },
104 | {
105 | key: Joi.alternatives([Joi.string(), Joi.binary()]).required(),
106 | algorithms: internals.schema.algorithms,
107 | kid: Joi.string()
108 | },
109 | {
110 | uri: Joi.string().uri().required(),
111 | rejectUnauthorized: Joi.boolean().default(true),
112 | headers: Joi.object().pattern(/.+/, Joi.string()),
113 | algorithms: internals.schema.algorithms
114 | }
115 | )
116 | .min(1)
117 | .single()
118 | .when('verify', { is: false, otherwise: Joi.required() }),
119 |
120 | unauthorizedAttributes: Joi.object().pattern(/.+/, Joi.string().allow(null, '')),
121 |
122 | validate: Joi.func().allow(false).required(),
123 |
124 | verify: Joi.object({
125 | aud: Joi.array().items(Joi.string(), Joi.object().instance(RegExp)).min(1).single().allow(false).required(),
126 | exp: Joi.boolean().default(true),
127 | iss: Joi.array().items(Joi.string()).min(1).single().allow(false).required(),
128 | nbf: Joi.boolean().default(true),
129 | sub: Joi.array().items(Joi.string()).min(1).single().allow(false).required(),
130 |
131 | maxAgeSec: Joi.number().integer().min(0).default(0),
132 | timeSkewSec: Joi.number().integer().min(0).default(0)
133 | })
134 | .when('.validate', { is: Joi.not(false), then: Joi.allow(false) })
135 | .required()
136 | });
137 |
138 |
139 | internals.implementation = function (server, options) {
140 |
141 | Hoek.assert(options, 'JWT authentication options missing');
142 |
143 | const settings = Joi.attempt(Hoek.clone(options), internals.schema.strategy);
144 | settings.headless = Token.headless(settings);
145 |
146 | const unauthorized = (message = null) => Boom.unauthorized(message, settings.httpAuthScheme, settings.unauthorizedAttributes);
147 | const missing = unauthorized();
148 |
149 | const provider = new Keys(server, settings);
150 |
151 | if (provider.hasJwks &&
152 | !server.plugins.jwt._caching) {
153 |
154 | server.plugins.jwt._caching = true;
155 | server.cache.provision({ provider: CatboxObject.Engine, name: server.plugins.jwt._cacheName });
156 | }
157 |
158 | return {
159 | authenticate: async function (request, h) {
160 |
161 | const result = { credentials: {} };
162 |
163 | // Extract token
164 |
165 | const token = internals.token(request, settings, missing, unauthorized);
166 |
167 | // Decode token
168 |
169 | try {
170 | result.artifacts = Token.decode(token, settings);
171 | }
172 | catch (err) {
173 | result.artifacts = err.artifacts;
174 | return h.unauthenticated(unauthorized(err.message), result);
175 | }
176 |
177 | // Obtain keys
178 |
179 | await provider.assign(result.artifacts, request);
180 | if (!result.artifacts.keys) {
181 | return h.unauthenticated(unauthorized(''), result);
182 | }
183 |
184 | // Verify token
185 |
186 | if (settings.verify) {
187 | try {
188 | Token.verifyPayload(result.artifacts, settings.verify);
189 | }
190 | catch (err) {
191 | return h.unauthenticated(unauthorized(err.message), result);
192 | }
193 |
194 | let valid = false;
195 | for (const key of result.artifacts.keys) {
196 | if (Crypto.verify(result.artifacts.raw, key.algorithm, key.key)) {
197 | valid = true;
198 | break;
199 | }
200 | }
201 |
202 | if (!valid) {
203 | return h.unauthenticated(unauthorized('Invalid token signature'), result);
204 | }
205 | }
206 |
207 | result.credentials = result.artifacts.decoded.payload;
208 |
209 | // Validate token
210 |
211 | if (settings.validate) {
212 | try {
213 | var { isValid, credentials, response } = await settings.validate(result.artifacts, request, h);
214 | }
215 | catch (err) {
216 | result.error = err;
217 | return h.unauthenticated(unauthorized(err.message), result);
218 | }
219 |
220 | if (response !== undefined) {
221 | return h.response(response).takeover();
222 | }
223 |
224 | if (credentials) {
225 | result.credentials = credentials;
226 | }
227 |
228 | if (!isValid) {
229 | return h.unauthenticated(unauthorized('Invalid credentials'), result);
230 | }
231 | }
232 |
233 | return h.authenticated(result);
234 | },
235 |
236 | verify: function (auth) {
237 |
238 | if (settings.verify) {
239 | Token.verifyTime(auth.artifacts, settings.verify);
240 | }
241 | }
242 | };
243 | };
244 |
245 |
246 | internals.token = function (request, settings, missing, unauthorized) {
247 |
248 | // Read the authentication token from the source depending upon the setting
249 |
250 | let authorization = null;
251 |
252 | if (settings.headerName) {
253 | authorization = request.headers[settings.headerName];
254 | }
255 | else {
256 | authorization = request.state[settings.cookieName];
257 | }
258 |
259 | if (!authorization) {
260 | throw missing;
261 | }
262 |
263 | // Authorization header will be like
264 |
265 | if (settings.headerName) {
266 | const parts = authorization.split(/\s+/);
267 | if (parts[0].toLowerCase() !== settings.httpAuthScheme.toLowerCase()) {
268 | throw missing;
269 | }
270 |
271 | if (parts.length !== 2) {
272 | throw unauthorized('Bad HTTP authentication header format');
273 | }
274 |
275 | const token = parts[1];
276 | if (!token) {
277 | throw missing;
278 | }
279 |
280 | return token;
281 | }
282 |
283 | return authorization;
284 | };
285 |
--------------------------------------------------------------------------------
/lib/token.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Bourne = require('@hapi/bourne');
4 |
5 | const Crypto = require('./crypto');
6 | const Keys = require('./keys');
7 | const Utils = require('./utils');
8 |
9 |
10 | const internals = {
11 | partRx: /^[\w\-]*$/,
12 | parts: ['header', 'payload', 'signature'],
13 | headless: Symbol('headless')
14 | };
15 |
16 |
17 | exports.generate = function (payload, secret, options = {}) {
18 |
19 | const { key, algorithms } = internals.secret(secret);
20 | let content = payload;
21 | const baseHeader = { alg: algorithms[0] };
22 |
23 | const clone = () => {
24 |
25 | if (content === payload) {
26 | content = Object.assign({}, content); // Shallow cloned
27 | }
28 | };
29 |
30 | if (content.iat === undefined &&
31 | options.iat !== false) {
32 |
33 | clone();
34 | content.iat = internals.tsSecs(options.now);
35 | }
36 |
37 | if (content.exp === undefined &&
38 | options.ttlSec) {
39 |
40 | clone();
41 | content.exp = options.ttlSec + internals.tsSecs(options.now);
42 | }
43 |
44 | if (options.typ !== false) {
45 | baseHeader.typ = 'JWT';
46 | }
47 |
48 | const header = Object.assign(baseHeader, options.header);
49 | const value = `${Utils.b64stringify(header)}.${Utils.b64stringify(content)}`;
50 | const signature = Crypto.generate(value, header.alg, key);
51 |
52 | if (options.headless === true) {
53 | const parts = value.split('.');
54 | return `${parts[1]}.${signature}`;
55 | }
56 |
57 | return `${value}.${signature}`;
58 | };
59 |
60 |
61 | exports.decode = function (token, options = {}) {
62 |
63 | const artifacts = {
64 | token,
65 | decoded: {},
66 | raw: {}
67 | };
68 |
69 | const parts = token.split('.');
70 |
71 | if (parts.length === 3) {
72 | if (options.headless) {
73 | throw internals.error('Token contains header', artifacts);
74 | }
75 |
76 | artifacts.raw = { header: parts[0], payload: parts[1], signature: parts[2] };
77 | artifacts.decoded.header = internals.b64parse(artifacts.raw.header);
78 | }
79 | else if (parts.length === 2 && options.headless) {
80 |
81 | const headless = exports.headless(options);
82 | artifacts.token = `${headless.raw}.${token}`;
83 | artifacts.raw = { header: headless.raw, payload: parts[0], signature: parts[1] };
84 | artifacts.decoded.header = headless.decoded;
85 | }
86 | else {
87 | throw internals.error('Invalid token structure', artifacts);
88 | }
89 |
90 | for (const part of internals.parts) {
91 | if (!internals.partRx.test(artifacts.raw[part])) {
92 | throw internals.error(`Invalid token ${part} part`, artifacts);
93 | }
94 | }
95 |
96 | artifacts.decoded.payload = internals.b64decode(artifacts.raw.payload);
97 | artifacts.decoded.signature = artifacts.raw.signature;
98 |
99 | const header = artifacts.decoded.header;
100 | if (!header) {
101 | throw internals.error('Invalid token missing header', artifacts);
102 | }
103 |
104 | const parsed = Bourne.safeParse(artifacts.decoded.payload);
105 | if (!parsed ||
106 | typeof parsed !== 'object' ||
107 | Array.isArray(parsed)) {
108 |
109 | throw internals.error('Invalid token payload', artifacts);
110 | }
111 |
112 | artifacts.decoded.payload = parsed;
113 |
114 | if (!artifacts.decoded.header.alg) {
115 | throw internals.error('Token header missing alg attribute', artifacts);
116 | }
117 |
118 | return artifacts;
119 | };
120 |
121 |
122 | exports.verify = function (artifacts, secret, options = {}) {
123 |
124 | exports.verifySignature(artifacts, secret);
125 | exports.verifyPayload(artifacts, options);
126 | };
127 |
128 |
129 | exports.verifySignature = function ({ decoded, raw }, secret) {
130 |
131 | const { key, algorithm } = internals.key(decoded, secret);
132 | if (!Crypto.verify(raw, algorithm, key)) {
133 | throw new Error('Invalid token signature');
134 | }
135 | };
136 |
137 |
138 | exports.verifyPayload = function ({ decoded }, options = {}) {
139 |
140 | const nowSec = internals.tsSecs(options.now);
141 | const skewSec = options.timeSkewSec ?? 0;
142 | const payload = decoded.payload;
143 |
144 | // Expiration and max age
145 |
146 | exports.verifyTime({ decoded }, options, nowSec);
147 |
148 | // Audience
149 |
150 | internals.audiance(payload.aud, options.aud);
151 |
152 | // Properties
153 |
154 | internals.match('iss', payload, options);
155 | internals.match('sub', payload, options);
156 | internals.match('jti', payload, options);
157 | internals.match('nonce', payload, options);
158 |
159 | // Not before
160 |
161 | if (options.nbf !== false &&
162 | payload.nbf !== undefined) {
163 |
164 | if (typeof payload.nbf !== 'number') {
165 | throw new Error('Invalid payload nbf value');
166 | }
167 |
168 | if (payload.nbf > nowSec + skewSec) {
169 | throw new Error('Token not yet active');
170 | }
171 | }
172 | };
173 |
174 |
175 | exports.verifyTime = function ({ decoded }, options = {}, _nowSec = null) {
176 |
177 | const nowSec = _nowSec ?? internals.tsSecs(options.now);
178 | const skewSec = options.timeSkewSec ?? 0;
179 | const payload = decoded.payload;
180 |
181 | // Expiration
182 |
183 | if (options.exp !== false &&
184 | payload.exp !== undefined) {
185 |
186 | if (typeof payload.exp !== 'number') {
187 | throw new Error('Invalid payload exp value');
188 | }
189 |
190 | if (payload.exp <= nowSec - skewSec) {
191 | throw new Error('Token expired');
192 | }
193 | }
194 |
195 | // Max age
196 |
197 | if (options.maxAgeSec) {
198 | if (!payload.iat ||
199 | typeof payload.iat !== 'number') {
200 |
201 | throw new Error('Missing or invalid payload iat value');
202 | }
203 |
204 | if (nowSec - payload.iat - skewSec > options.maxAgeSec) {
205 | throw new Error('Token maximum age exceeded');
206 | }
207 | }
208 | };
209 |
210 |
211 | exports.headless = function (options) {
212 |
213 | const headless = options.headless;
214 | if (!headless) {
215 | return null;
216 | }
217 |
218 | if (typeof headless === 'object') {
219 | if (headless[internals.headless]) {
220 | return headless;
221 | }
222 |
223 | return {
224 | [internals.headless]: true,
225 | raw: Buffer.from(JSON.stringify(headless)).toString('base64'),
226 | decoded: headless
227 | };
228 | }
229 |
230 | return {
231 | [internals.headless]: true,
232 | raw: headless,
233 | decoded: internals.b64parse(headless)
234 | };
235 | };
236 |
237 |
238 | internals.error = function (message, artifacts) {
239 |
240 | const error = new Error(message);
241 | error.artifacts = artifacts;
242 | return error;
243 | };
244 |
245 |
246 | internals.b64decode = function (string) {
247 |
248 | return Buffer.from(string, 'base64').toString();
249 | };
250 |
251 |
252 | internals.b64parse = function (string) {
253 |
254 | return Bourne.safeParse(internals.b64decode(string));
255 | };
256 |
257 |
258 | internals.key = function (decoded, secret) {
259 |
260 | const { key, algorithms } = internals.secret(secret);
261 | if (!algorithms.includes(decoded.header.alg)) {
262 | throw new Error('Unsupported algorithm');
263 | }
264 |
265 | return { key, algorithm: decoded.header.alg };
266 | };
267 |
268 |
269 | internals.secret = function (secret) {
270 |
271 | const set = (typeof secret === 'object' && secret.asymmetricKeyType) || typeof secret === 'string' || Buffer.isBuffer(secret) ? { key: secret } : secret;
272 |
273 | return {
274 | key: set.key,
275 | algorithms: set.algorithm ? [set.algorithm] : set.algorithms || Keys.keyAlgorithms(set.key)
276 | };
277 | };
278 |
279 |
280 | internals.audiance = function (aud, audiences) {
281 |
282 | if (!audiences) {
283 | return;
284 | }
285 |
286 | if (aud === undefined) {
287 | throw new Error('Token missing payload aud value');
288 | }
289 |
290 | const auds = Array.isArray(aud) ? aud : [aud];
291 | audiences = Array.isArray(audiences) ? audiences : [audiences];
292 |
293 | for (const compare of auds) {
294 | for (const match of audiences) {
295 | if (typeof match === 'string') {
296 | if (compare === match) {
297 | return;
298 | }
299 | }
300 | else {
301 | if (match.test(compare)) {
302 | return;
303 | }
304 | }
305 | }
306 | }
307 |
308 | throw new Error('Token audience is not allowed');
309 | };
310 |
311 |
312 | internals.match = function (type, payload, options) {
313 |
314 | const matchTo = options[type];
315 | if (!matchTo) {
316 | return;
317 | }
318 |
319 | const value = payload[type];
320 | if (value === undefined) {
321 | throw new Error(`Token missing payload ${type} value`);
322 | }
323 |
324 | if (Array.isArray(matchTo) && matchTo.includes(value) ||
325 | matchTo === value) {
326 |
327 | return;
328 | }
329 |
330 | throw new Error(`Token payload ${type} value not allowed`);
331 | };
332 |
333 |
334 | internals.tsSecs = function (ts) {
335 |
336 | return Math.floor((ts ?? Date.now()) / 1000);
337 | };
338 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const B64 = require('@hapi/b64');
4 | const Joi = require('joi');
5 |
6 |
7 | const internals = {};
8 |
9 |
10 | exports.b64stringify = function (obj) {
11 |
12 | return B64.base64urlEncode(JSON.stringify(obj), 'utf-8');
13 | };
14 |
15 |
16 | exports.toHex = function (number) {
17 |
18 | const nstr = number.toString(16);
19 | if (nstr.length % 2) {
20 | return `0${nstr}`;
21 | }
22 |
23 | return nstr;
24 | };
25 |
26 | // Refer for header name pattern reference https://github.com/nodejs/node/blob/7ef069e483e015a803660125cdbfa81ddfa0357b/lib/_http_common.js#L203
27 |
28 | exports.validHttpTokenSchema = Joi.string().pattern(/^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/);
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@hapi/jwt",
3 | "description": "JWT (JSON Web Token) Authentication",
4 | "version": "3.2.0",
5 | "repository": "git://github.com/hapijs/jwt",
6 | "main": "lib/index.js",
7 | "types": "lib/index.d.ts",
8 | "files": [
9 | "lib"
10 | ],
11 | "keywords": [
12 | "jwt",
13 | "authentication",
14 | "plugin",
15 | "hapi"
16 | ],
17 | "eslintConfig": {
18 | "extends": [
19 | "plugin:@hapi/module"
20 | ]
21 | },
22 | "dependencies": {
23 | "@hapi/b64": "^6.0.0",
24 | "@hapi/boom": "^10.0.0",
25 | "@hapi/bounce": "^3.0.0",
26 | "@hapi/bourne": "^3.0.0",
27 | "@hapi/catbox-object": "^3.0.0",
28 | "@hapi/cryptiles": "^6.0.0",
29 | "@hapi/hoek": "^10.0.0",
30 | "@hapi/wreck": "^18.0.0",
31 | "ecdsa-sig-formatter": "^1.0.0",
32 | "joi": "^17.2.1"
33 | },
34 | "devDependencies": {
35 | "@hapi/code": "^9.0.0",
36 | "@hapi/eslint-plugin": "^6.0.0",
37 | "@hapi/hapi": "^21.0.0",
38 | "@hapi/lab": "^25.0.1",
39 | "node-forge": "^1.0.0",
40 | "node-rsa": "^1.0.0"
41 | },
42 | "scripts": {
43 | "test": "lab -a @hapi/code -t 100 -L -m 10000",
44 | "test-cov-html": "lab -a @hapi/code -r html -o coverage.html"
45 | },
46 | "license": "BSD-3-Clause"
47 | }
48 |
--------------------------------------------------------------------------------
/test/crypto.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Jwt = require('..');
5 | const Lab = require('@hapi/lab');
6 |
7 | const Mock = require('./mock');
8 |
9 |
10 | const internals = {};
11 |
12 |
13 | const { describe, it } = exports.lab = Lab.script();
14 | const expect = Code.expect;
15 |
16 |
17 | describe('Crypto', () => {
18 |
19 | describe('RSA', () => {
20 |
21 | const algorithms = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512'];
22 | for (const algorithm of algorithms) {
23 | it(`supports ${algorithm}`, () => {
24 |
25 | const header = 'abc';
26 | const payload = '123';
27 | const value = `${header}.${payload}`;
28 |
29 | const pair = Mock.pair();
30 | const signature = Jwt.token.signature.generate(value, algorithm, pair.private);
31 | expect(Jwt.token.signature.verify({ header, payload, signature }, algorithm, pair.public)).to.be.true();
32 | });
33 | }
34 | });
35 |
36 | describe('EC', () => {
37 |
38 | const algorithms = ['ES256', 'ES384', 'ES512'];
39 | for (const algorithm of algorithms) {
40 | it(`supports ${algorithm}`, () => {
41 |
42 | const header = 'abc';
43 | const payload = '123';
44 | const value = `${header}.${payload}`;
45 |
46 | const pair = Mock.pair('ec', algorithm.slice(2));
47 | const signature = Jwt.token.signature.generate(value, algorithm, pair.private);
48 | expect(Jwt.token.signature.verify({ header, payload, signature }, algorithm, pair.public)).to.be.true();
49 | });
50 | }
51 | });
52 |
53 | describe('EdDSA', () => {
54 |
55 | const alg = 'EdDSA';
56 | const curves = ['ed25519', 'ed448'];
57 | for (const crv of curves) {
58 | it(`supports ${alg} ${crv}`, () => {
59 |
60 | const header = 'abc';
61 | const payload = '123';
62 | const value = `${header}.${payload}`;
63 |
64 | const pair = Mock.pair(alg, crv);
65 | const signature = Jwt.token.signature.generate(value, alg, pair.private);
66 | expect(Jwt.token.signature.verify({ header, payload, signature }, alg, pair.public)).to.be.true();
67 | });
68 | }
69 | });
70 |
71 |
72 | describe('generate()', () => {
73 |
74 | it('errors on unsupported algorithm', () => {
75 |
76 | expect(() => Jwt.token.signature.generate('abc', 'unknown', 'secret')).to.throw('Unsupported algorithm');
77 | });
78 | });
79 |
80 | describe('verify()', () => {
81 |
82 | it('errors on unsupported algorithm', () => {
83 |
84 | expect(() => Jwt.token.signature.verify({}, 'unknown', 'secret')).to.throw('Unsupported algorithm');
85 | });
86 | });
87 |
88 | describe('rsaPublicKeyToPEM()', () => {
89 |
90 | it('converts public key to PEM', () => {
91 |
92 | const e = 'AQAB';
93 | const n = 'ALmBQblv13+ReieM3zRFTSvm+TUmF/o465Y3tKijFUkjTdyYyOeY7jQoau6Af917/TJtEC4XtQyBBY0GZU7KPN0s9a+h4/lLfqNTAfPcWjxm7aoWOtWbqRUpqlm38knwm6BZ2zyc/9rsvfNXWx6fgs47W9n6i1RyVuAHkuj4pMmstiLWDmmhrOH8Sz7L1iZwkwPU6AczILiHW0PJLlu7a0lVeYAB0FOETXu9iDPXhi4z6m35UIhEa343S7fp1z4k+R9oxdr5n92A1nNNV+6Kkpi+s8E+ukiFcbEJkCLvGt8uPxQvqUJVavHtHDX2aqVJnORydluTnPTlhU+aJ9F9JQs=';
94 |
95 | expect(Jwt.crypto.rsaPublicKeyToPEM(n, e)).to.equal('-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAuYFBuW/Xf5F6J4zfNEVNK+b5NSYX+jjrlje0qKMVSSNN3JjI55ju\nNChq7oB/3Xv9Mm0QLhe1DIEFjQZlTso83Sz1r6Hj+Ut+o1MB89xaPGbtqhY61Zup\nFSmqWbfySfCboFnbPJz/2uy981dbHp+Czjtb2fqLVHJW4AeS6Pikyay2ItYOaaGs\n4fxLPsvWJnCTA9ToBzMguIdbQ8kuW7trSVV5gAHQU4RNe72IM9eGLjPqbflQiERr\nfjdLt+nXPiT5H2jF2vmf3YDWc01X7oqSmL6zwT66SIVxsQmQIu8a3y4/FC+pQlVq\n8e0cNfZqpUmc5HJ2W5Oc9OWFT5on0X0lCwIDAQAB\n-----END RSA PUBLIC KEY-----\n');
96 | });
97 |
98 | it('converts public key to PEM (letter msb)', () => {
99 |
100 | const e = 'AQAB';
101 | const n = 's4W7xjkQZP3OwG7PfRgcYKn8eRYXHiz1iK503fS-K2FZo-Ublwwa2xFZWpsUU_jtoVCwIkaqZuo6xoKtlMYXXvfVHGuKBHEBVn8b8x_57BQWz1d0KdrNXxuMvtFe6RzMqiMqzqZrzae4UqVCkYqcR9gQx66Ehq7hPmCxJCkg7ajo7fu6E7dPd34KH2HSYRsaaEA_BcKTeb9H1XE_qEKjog68wUU9Ekfl3FBIRN-1Ah_BoktGFoXyi_jt0-L0-gKcL1BLmUlGzMusvRbjI_0-qj-mc0utGdRjY-xIN2yBj8vl4DODO-wMwfp-cqZbCd9TENyHaTb8iA27s-73L3ExOQ';
102 |
103 | expect(Jwt.crypto.rsaPublicKeyToPEM(n, e)).to.equal('-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAs4W7xjkQZP3OwG7PfRgcYKn8eRYXHiz1iK503fS+K2FZo+Ublwwa\n2xFZWpsUU/jtoVCwIkaqZuo6xoKtlMYXXvfVHGuKBHEBVn8b8x/57BQWz1d0KdrN\nXxuMvtFe6RzMqiMqzqZrzae4UqVCkYqcR9gQx66Ehq7hPmCxJCkg7ajo7fu6E7dP\nd34KH2HSYRsaaEA/BcKTeb9H1XE/qEKjog68wUU9Ekfl3FBIRN+1Ah/BoktGFoXy\ni/jt0+L0+gKcL1BLmUlGzMusvRbjI/0+qj+mc0utGdRjY+xIN2yBj8vl4DODO+wM\nwfp+cqZbCd9TENyHaTb8iA27s+73L3ExOQIDAQAB\n-----END RSA PUBLIC KEY-----\n');
104 | });
105 | });
106 | });
107 |
--------------------------------------------------------------------------------
/test/esm.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Lab = require('@hapi/lab');
5 |
6 |
7 | const { before, describe, it } = exports.lab = Lab.script();
8 | const expect = Code.expect;
9 |
10 |
11 | describe('import()', () => {
12 |
13 | let Jwt;
14 |
15 | before(async () => {
16 |
17 | Jwt = await import('../lib/index.js');
18 | });
19 |
20 | it('exposes all methods and classes as named imports', () => {
21 |
22 | expect(Object.keys(Jwt)).to.equal([
23 | 'crypto',
24 | 'default',
25 | 'plugin',
26 | 'token',
27 | 'utils'
28 | ]);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/keys.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Hapi = require('@hapi/hapi');
5 | const Hoek = require('@hapi/hoek');
6 | const Jwt = require('..');
7 | const Lab = require('@hapi/lab');
8 |
9 | const Mock = require('./mock');
10 |
11 |
12 | const internals = {};
13 |
14 |
15 | const { describe, it } = exports.lab = Lab.script();
16 | const expect = Code.expect;
17 |
18 |
19 | describe('Keys', () => {
20 |
21 | it('processes key array', async () => {
22 |
23 | const jwks1 = await Mock.jwks({ algorithm: 'RS256', crap: true });
24 | const jwks2 = await Mock.jwks({ algorithm: 'RS256', kid: jwks1.kid });
25 | const jwks3 = await Mock.jwks({ algorithm: 'RS256' });
26 |
27 | const keys = [
28 | 'some_shared_secret',
29 | Buffer.from('another_shared_secret'),
30 | {
31 | key: 'wrapped_secret_without_algorithms',
32 | kid: 'explicit'
33 | },
34 | {
35 | key: Buffer.from('wrapped_buffer_without_algorithms'),
36 | kid: 'some'
37 | },
38 | {
39 | key: 'wrapped_secret_with_algorithms',
40 | algorithms: ['HS256', 'HS384']
41 | },
42 | {
43 | key: Buffer.from('wrapped_buffer_with_algorithms'),
44 | algorithms: ['HS384']
45 | },
46 | (artifacts, request) => {
47 |
48 | return [
49 | { key: 'dynamic_secret', algorithms: ['HS256'] },
50 | '',
51 | Buffer.from('some_other_secret')
52 | ];
53 | },
54 | (artifacts, request) => {
55 |
56 | return {
57 | key: 'dynamic_secret',
58 | algorithms: ['HS384']
59 | };
60 | },
61 | {
62 | uri: jwks1.endpoint
63 | },
64 | {
65 | uri: jwks2.endpoint,
66 | algorithms: ['RS256', 'RS512']
67 | },
68 | {
69 | uri: jwks3.endpoint
70 | },
71 | {
72 | key: '',
73 | algorithms: ['none']
74 | }
75 | ];
76 |
77 | const server = Hapi.server();
78 | await server.register(Jwt);
79 |
80 | server.auth.strategy('jwt', 'jwt', {
81 | keys,
82 | verify: {
83 | aud: false,
84 | iss: false,
85 | sub: false
86 | },
87 | validate: false
88 | });
89 |
90 | server.auth.default('jwt');
91 |
92 | await server.initialize();
93 |
94 | const provider = server.plugins.jwt._providers[0];
95 |
96 | const tests = [
97 | [{ decoded: { header: { alg: 'HS256' } } }, 7],
98 | [{ decoded: { header: { alg: 'HS256', kid: 'some' } } }, 6],
99 | [{ decoded: { header: { alg: 'HS384' } } }, 8],
100 | [{ decoded: { header: { alg: 'RS256', kid: jwks1.kid } } }, 2],
101 | [{ decoded: { header: { alg: 'RS256', kid: 'other' } } }, 0],
102 | [{ decoded: { header: { alg: 'RS256' } } }, 0]
103 | ];
104 |
105 | for (const [artifacts, count] of tests) {
106 | await provider.assign(artifacts, {});
107 | if (count) {
108 | expect(artifacts.keys).to.have.length(count);
109 | }
110 | else {
111 | expect(artifacts.keys).to.not.exist();
112 | }
113 | }
114 |
115 | await jwks1.server.stop();
116 | await jwks2.server.stop();
117 | await jwks3.server.stop();
118 | });
119 |
120 | describe('assign()', () => {
121 |
122 | it('reports remote source error', async () => {
123 |
124 | const jwks = await Mock.jwks();
125 |
126 | const server = Hapi.server();
127 | await server.register(Jwt);
128 |
129 | server.auth.strategy('jwt', 'jwt', {
130 | keys: [
131 | {
132 | uri: jwks.endpoint
133 | },
134 | jwks.key.public
135 | ],
136 | verify: {
137 | aud: false,
138 | iss: false,
139 | sub: false
140 | },
141 | validate: false,
142 | cache: {
143 | expiresIn: 10,
144 | staleIn: 5,
145 | staleTimeout: 1,
146 | generateTimeout: 10000 // Extra large for Windows to avoid catbox cache timeout waiting for disconnected error
147 | }
148 | });
149 |
150 | await server.initialize();
151 |
152 | await jwks.server.stop();
153 | await Hoek.wait(10);
154 |
155 | const provider = server.plugins.jwt._providers[0];
156 | const artifacts = { decoded: { header: { alg: 'RS256', kid: jwks.kid } } };
157 | await provider.assign(artifacts, {});
158 |
159 | expect(artifacts.errors).to.have.length(1);
160 | expect(artifacts.errors[0]).to.be.an.error('JWKS endpoint error');
161 | });
162 |
163 | it('skips failing remote source', async () => {
164 |
165 | const jwks = await Mock.jwks();
166 |
167 | const server = Hapi.server();
168 | await server.register(Jwt);
169 |
170 | server.auth.strategy('jwt', 'jwt', {
171 | keys: {
172 | uri: jwks.endpoint
173 | },
174 | verify: {
175 | aud: false,
176 | iss: false,
177 | sub: false
178 | },
179 | validate: false,
180 | cache: {
181 | expiresIn: 10,
182 | staleIn: 5,
183 | staleTimeout: 1,
184 | generateTimeout: 100
185 | }
186 | });
187 |
188 | await server.initialize();
189 |
190 | await jwks.server.stop();
191 | await Hoek.wait(10);
192 |
193 | const provider = server.plugins.jwt._providers[0];
194 | const artifacts = { decoded: { header: { alg: 'RS256', kid: jwks.kid } } };
195 |
196 | await expect(provider.assign(artifacts, {})).to.reject('Failed to obtain keys');
197 | });
198 |
199 | it('skips failing dynamic source', async () => {
200 |
201 | const server = Hapi.server();
202 | await server.register(Jwt);
203 |
204 | server.auth.strategy('jwt', 'jwt', {
205 | keys: () => {
206 |
207 | throw new Error('sorry');
208 | },
209 | verify: {
210 | aud: false,
211 | iss: false,
212 | sub: false
213 | },
214 | validate: false,
215 | cache: {
216 | expiresIn: 10,
217 | staleIn: 5,
218 | staleTimeout: 1,
219 | generateTimeout: 100
220 | }
221 | });
222 |
223 | await server.initialize();
224 |
225 | const provider = server.plugins.jwt._providers[0];
226 | const artifacts = { decoded: { header: { alg: 'HS256' } } };
227 |
228 | await expect(provider.assign(artifacts, {})).to.reject('Failed to obtain keys');
229 | });
230 | });
231 |
232 | describe('jwks()', () => {
233 |
234 | it('reports remote source missing payload error', async () => {
235 |
236 | const jwks = Hapi.server();
237 | const path = '/.well-known/jwks.json';
238 | jwks.route({ method: 'GET', path, handler: () => '' });
239 | await jwks.start();
240 |
241 | const server = Hapi.server();
242 | await server.register(Jwt);
243 |
244 | server.auth.strategy('jwt', 'jwt', {
245 | keys: {
246 | uri: `http://0.0.0.0:${jwks.info.port}${path}`
247 | },
248 | verify: {
249 | aud: false,
250 | iss: false,
251 | sub: false
252 | },
253 | validate: false
254 | });
255 |
256 | await expect(server.initialize()).to.reject('JWKS endpoint returned empty payload');
257 | await jwks.stop();
258 | });
259 |
260 | it('reports remote source invalid payload error', async () => {
261 |
262 | for (const payload of [{}, { keys: 123 }, { keys: [] }]) {
263 | const jwks = Hapi.server();
264 | const path = '/.well-known/jwks.json';
265 | jwks.route({ method: 'GET', path, handler: () => payload });
266 | await jwks.start();
267 |
268 | const server = Hapi.server();
269 | await server.register(Jwt);
270 |
271 | server.auth.strategy('jwt', 'jwt', {
272 | keys: {
273 | uri: `http://0.0.0.0:${jwks.info.port}${path}`
274 | },
275 | verify: {
276 | aud: false,
277 | iss: false,
278 | sub: false
279 | },
280 | validate: false
281 | });
282 |
283 | await expect(server.initialize()).to.reject('JWKS endpoint returned invalid payload');
284 | await jwks.stop();
285 | }
286 | });
287 |
288 | it('errors on no valid rsa keys', async () => {
289 |
290 | const jwks = await Mock.jwks({ algorithm: 'RS512', public: false });
291 |
292 | const server = Hapi.server();
293 | await server.register(Jwt);
294 |
295 | server.auth.strategy('jwt', 'jwt', {
296 | keys: {
297 | uri: jwks.endpoint,
298 | algorithms: ['RS256'] // Does not match - always ignored
299 | },
300 | verify: {
301 | aud: false,
302 | iss: false,
303 | sub: false
304 | },
305 | validate: false
306 | });
307 |
308 | await expect(server.initialize()).to.reject('JWKS endpoint response contained no valid keys');
309 | await jwks.server.stop();
310 | });
311 | });
312 |
313 | describe('algorithms()', () => {
314 |
315 | it('sets default algorithms for rsa', async () => {
316 |
317 | const jwks = await Mock.jwks({ public: false, algorithm: false });
318 |
319 | const server = Hapi.server();
320 | await server.register(Jwt);
321 |
322 | server.auth.strategy('jwt', 'jwt', {
323 | keys: [
324 | {
325 | uri: jwks.endpoint
326 | }
327 | ],
328 | verify: {
329 | aud: false,
330 | iss: false,
331 | sub: false
332 | },
333 | validate: false
334 | });
335 |
336 | await server.initialize();
337 |
338 | const provider = server.plugins.jwt._providers[0];
339 | expect((await provider._cache.get(jwks.endpoint)).get(jwks.kid).algorithms).to.equal(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']);
340 |
341 | await jwks.server.stop();
342 | });
343 |
344 | it('sets default algorithms for public', async () => {
345 |
346 | const jwks = await Mock.jwks({ rsa: false, algorithm: false });
347 |
348 | const server = Hapi.server();
349 | await server.register(Jwt);
350 |
351 | server.auth.strategy('jwt', 'jwt', {
352 | keys: [
353 | {
354 | uri: jwks.endpoint
355 | }
356 | ],
357 | verify: {
358 | aud: false,
359 | iss: false,
360 | sub: false
361 | },
362 | validate: false
363 | });
364 |
365 | await server.initialize();
366 |
367 | const provider = server.plugins.jwt._providers[0];
368 | expect((await provider._cache.get(jwks.endpoint)).get(jwks.kid).algorithms).to.equal(['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512', 'EdDSA']);
369 |
370 | await jwks.server.stop();
371 | });
372 |
373 | it('use specified algorithms for rsa', async () => {
374 |
375 | const jwks = await Mock.jwks({ public: false, algorithm: false });
376 |
377 | const server = Hapi.server();
378 | await server.register(Jwt);
379 |
380 | server.auth.strategy('jwt', 'jwt', {
381 | keys: [
382 | {
383 | uri: jwks.endpoint,
384 | algorithms: ['RS512', 'PS512']
385 | }
386 | ],
387 | verify: {
388 | aud: false,
389 | iss: false,
390 | sub: false
391 | },
392 | validate: false
393 | });
394 |
395 | await server.initialize();
396 |
397 | const provider = server.plugins.jwt._providers[0];
398 | expect((await provider._cache.get(jwks.endpoint)).get(jwks.kid).algorithms).to.equal(['RS512', 'PS512']);
399 |
400 | await jwks.server.stop();
401 | });
402 | });
403 | });
404 |
--------------------------------------------------------------------------------
/test/mock.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Crypto = require('crypto');
4 | const Cryptiles = require('@hapi/cryptiles');
5 | const Forge = require('node-forge');
6 | const Hapi = require('@hapi/hapi');
7 | const Jwt = require('..');
8 | const Rsa = require('node-rsa');
9 |
10 |
11 | const internals = {};
12 |
13 |
14 | exports.jwks = async function (options = {}) {
15 |
16 | const server = Hapi.server();
17 | const path = '/.well-known/jwks.json';
18 | server.route({ method: 'GET', path, handler: () => server.app.jwks });
19 | await server.start();
20 |
21 | const key = {
22 | kid: options.kid || Cryptiles.randomString(24),
23 | kty: 'RSA',
24 | use: 'sig'
25 | };
26 |
27 | if (options.algorithm !== false) {
28 | key.alg = options.algorithm || 'RS256';
29 | }
30 |
31 | server.app.jwks = { keys: [key] };
32 |
33 | const pair = exports.pair();
34 |
35 | if (options.public !== false) {
36 | const now = new Date();
37 | const cert = Forge.pki.createCertificate();
38 | cert.publicKey = Forge.pki.publicKeyFromPem(pair.public);
39 | cert.serialNumber = parseInt(Cryptiles.randomDigits(15)).toString(16);
40 | cert.validity.notBefore = now;
41 | cert.validity.notAfter = now;
42 | cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
43 | cert.setSubject([{ name: 'commonName', value: 'http://example.com' }]);
44 | cert.sign(Forge.pki.privateKeyFromPem(pair.private));
45 | const certDer = Forge.util.encode64(Forge.asn1.toDer(Forge.pki.certificateToAsn1(cert)).getBytes());
46 | key.x5c = [certDer];
47 | key.x5t = key.kid;
48 | }
49 |
50 | if (options.rsa !== false) {
51 | const rsa = new Rsa();
52 | rsa.importKey(pair.private);
53 | const { n, e } = rsa.exportKey('components');
54 | key.e = Buffer.isBuffer(e) ? e.toString('base64') : Buffer.from(Jwt.utils.toHex(e), 'hex').toString('base64');
55 | key.n = n.toString('base64');
56 | }
57 |
58 | if (options.crap) {
59 | server.app.jwks.keys.push({ use: 'mock' });
60 | server.app.jwks.keys.push({ use: 'sig', kty: 'OTHER' });
61 | server.app.jwks.keys.push({ use: 'sig', kty: 'RSA' });
62 | server.app.jwks.keys.push({ use: 'sig', kty: 'RSA', kid: 'test', x5c: [] });
63 | server.app.jwks.keys.push({ use: 'sig', kty: 'RSA', kid: 'test', n: 'b64' });
64 | }
65 |
66 | return {
67 | server,
68 | // This is the host specifically for node v18 w/ hapi v20, re: default host and ipv6 support. See also hapijs/hapi#4357.
69 | endpoint: `http://0.0.0.0:${server.info.port}${path}`,
70 | kid: key.kid,
71 | key: pair
72 | };
73 | };
74 |
75 |
76 | exports.pair = function (type = 'rsa', bits = 2048) {
77 |
78 | // RSA
79 |
80 | if (type === 'rsa') {
81 | const pair = Forge.pki.rsa.generateKeyPair(bits);
82 | const keys = {
83 | private: Forge.pki.privateKeyToPem(pair.privateKey),
84 | public: Forge.pki.publicKeyToPem(pair.publicKey)
85 | };
86 |
87 | return keys;
88 | }
89 |
90 | // EdDSA - ed25519
91 | // EdDSA - ed448
92 |
93 | if (type === 'EdDSA') {
94 | const crv = bits;
95 |
96 | const { privateKey, publicKey } = Crypto.generateKeyPairSync(crv);
97 | const keys = {
98 | private: privateKey,
99 | public: publicKey
100 | };
101 |
102 | return keys;
103 | }
104 |
105 | // EC
106 |
107 | return internals.cert[type][bits];
108 | };
109 |
110 |
111 | internals.cert = {
112 | ec: {
113 | 256: {},
114 | 384: {},
115 | 512: {}
116 | }
117 | };
118 |
119 |
120 | // openssl ecparam -name prime256v1 -genkey
121 |
122 | internals.cert.ec['256'].private = `-----BEGIN EC PARAMETERS-----
123 | BggqhkjOPQMBBw==
124 | -----END EC PARAMETERS-----
125 | -----BEGIN EC PRIVATE KEY-----
126 | MHcCAQEEILQzd1wrxa3MtO46qgOwK2xyVbXUn3J/Bz1E51sAZfCsoAoGCCqGSM49
127 | AwEHoUQDQgAEMPMwBlhlHhfWY9S8g35VIbiyq121JGeYEctjKnAuMqOsT05xLsWR
128 | xP87kTuGZned4BPFbYnUHIXlDCKidFWQeg==
129 | -----END EC PRIVATE KEY-----`;
130 |
131 |
132 | // openssl ec -in private.pem -pubout
133 |
134 | internals.cert.ec['256'].public = `-----BEGIN PUBLIC KEY-----
135 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMPMwBlhlHhfWY9S8g35VIbiyq121
136 | JGeYEctjKnAuMqOsT05xLsWRxP87kTuGZned4BPFbYnUHIXlDCKidFWQeg==
137 | -----END PUBLIC KEY-----`;
138 |
139 |
140 | // openssl ecparam -name secp384r1 -genkey
141 |
142 | internals.cert.ec['384'].private = `-----BEGIN EC PARAMETERS-----
143 | BgUrgQQAIg==
144 | -----END EC PARAMETERS-----
145 | -----BEGIN EC PRIVATE KEY-----
146 | MIGkAgEBBDAhNOjAJzy3C9Q5nMzULpoTi+Dq7DAckG01kv4+KOz8EU1uJUuwKaE2
147 | g04RIhELbzOgBwYFK4EEACKhZANiAATBgd1i3IoRpHQKQh4nQBlZahhicDp0Z3rv
148 | 8isjvXzanp/qi6+jy+cqozNgTYW6EPb0iXFjr7tK3sDWqLzn+XSV4ExfLZnI77EF
149 | Xp4efGx39zTeet5g2d+FiPhS7eDGoMg=
150 | -----END EC PRIVATE KEY-----`;
151 |
152 |
153 | // openssl ec -in private.pem -pubout
154 |
155 | internals.cert.ec['384'].public = `-----BEGIN PUBLIC KEY-----
156 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEwYHdYtyKEaR0CkIeJ0AZWWoYYnA6dGd6
157 | 7/IrI7182p6f6ouvo8vnKqMzYE2FuhD29IlxY6+7St7A1qi85/l0leBMXy2ZyO+x
158 | BV6eHnxsd/c03nreYNnfhYj4Uu3gxqDI
159 | -----END PUBLIC KEY-----`;
160 |
161 |
162 | // openssl ecparam -name secp521r1 -genkey
163 |
164 | internals.cert.ec['512'].private = `-----BEGIN EC PARAMETERS-----
165 | BgUrgQQAIw==
166 | -----END EC PARAMETERS-----
167 | -----BEGIN EC PRIVATE KEY-----
168 | MIHcAgEBBEIAPcFy9KaAf2xDTLrmMq/3xEw6MOgUBgrjdehScfbDcoeypJNFuGBt
169 | XXFw0oTkm7zXHmtOU1jOVSKAiNm2lBL+jk2gBwYFK4EEACOhgYkDgYYABAF+MMlz
170 | GpiYmAij2dDzeBJAbj2Bdip+uUjekEA4clOtSDQz6F+Nxqfj2fZUk1x/Kd+C5dZ5
171 | iE+uX7FqYDxyra1f3QB1KpicJ/q1bhiDn9nuBDyEglSlDYbYC59hYdarMZcW6DvA
172 | Nc6GnwTIxJr/O0X7geS7YOVtSsyYcNnwxumnKkT4Aw==
173 | -----END EC PRIVATE KEY-----`;
174 |
175 |
176 | // openssl ec -in private.pem -pubout
177 |
178 | internals.cert.ec['512'].public = `-----BEGIN PUBLIC KEY-----
179 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBfjDJcxqYmJgIo9nQ83gSQG49gXYq
180 | frlI3pBAOHJTrUg0M+hfjcan49n2VJNcfynfguXWeYhPrl+xamA8cq2tX90AdSqY
181 | nCf6tW4Yg5/Z7gQ8hIJUpQ2G2AufYWHWqzGXFug7wDXOhp8EyMSa/ztF+4Hku2Dl
182 | bUrMmHDZ8MbppypE+AM=
183 | -----END PUBLIC KEY-----`;
184 |
--------------------------------------------------------------------------------
/test/plugin.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Hapi = require('@hapi/hapi');
5 | const Hoek = require('@hapi/hoek');
6 | const Jwt = require('..');
7 | const Lab = require('@hapi/lab');
8 |
9 | const Mock = require('./mock');
10 |
11 |
12 | const internals = {};
13 |
14 |
15 | const { describe, it } = exports.lab = Lab.script();
16 | const expect = Code.expect;
17 |
18 |
19 | describe('Plugin', () => {
20 |
21 | it('authenticates a request (HS256)', async () => {
22 |
23 | const secret = 'some_shared_secret';
24 |
25 | const server = Hapi.server();
26 | await server.register(Jwt);
27 |
28 | server.auth.strategy('jwt', 'jwt', {
29 | keys: secret,
30 | verify: {
31 | aud: 'urn:audience:test',
32 | iss: 'urn:issuer:test',
33 | sub: false
34 | },
35 | validate: (artifacts, request, h) => {
36 |
37 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
38 | }
39 | });
40 |
41 | server.auth.default('jwt');
42 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
43 |
44 | const token1 = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
45 | const res1 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token1}` } });
46 | expect(res1.result).to.equal('steve');
47 |
48 | const token2 = Jwt.token.generate({ user: 'steve' }, secret);
49 | const res2 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token2}` } });
50 | expect(res2.statusCode).to.equal(401);
51 |
52 | const res3 = await server.inject('/');
53 | expect(res3.statusCode).to.equal(401);
54 |
55 | const res4 = await server.inject({ url: '/', headers: { authorization: 'Bearer' } });
56 | expect(res4.statusCode).to.equal(401);
57 |
58 | const res5 = await server.inject({ url: '/', headers: { authorization: 'Bearer ' } });
59 | expect(res5.statusCode).to.equal(401);
60 |
61 | const res6 = await server.inject({ url: '/', headers: { authorization: 'Bearer a.b' } });
62 | expect(res6.statusCode).to.equal(401);
63 |
64 | const res7 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token1}x` } });
65 | expect(res7.statusCode).to.equal(401);
66 | });
67 |
68 | it('authenticates a request (RSA256 public)', async () => {
69 |
70 | const jwks = await Mock.jwks({ rsa: false, public: true });
71 |
72 | const server = Hapi.server();
73 | await server.register(Jwt);
74 |
75 | server.auth.strategy('jwt', 'jwt', {
76 | keys: {
77 | uri: jwks.endpoint
78 | },
79 | verify: {
80 | aud: 'urn:audience:test',
81 | iss: 'urn:issuer:test',
82 | sub: false
83 | },
84 | validate: (artifacts, request, h) => {
85 |
86 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
87 | }
88 | });
89 |
90 | server.auth.default('jwt');
91 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
92 |
93 | await server.initialize();
94 |
95 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, jwks.key.private, { header: { kid: jwks.kid } });
96 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
97 | expect(res.result).to.equal('steve');
98 |
99 | await jwks.server.stop();
100 | });
101 |
102 | it('authenticates a request (RSA256 rsa)', async () => {
103 |
104 | const jwks = await Mock.jwks({ rsa: true, public: false });
105 |
106 | const server = Hapi.server();
107 | await server.register(Jwt);
108 |
109 | server.auth.strategy('jwt', 'jwt', {
110 | keys: {
111 | uri: jwks.endpoint
112 | },
113 | verify: {
114 | aud: 'urn:audience:test',
115 | iss: 'urn:issuer:test',
116 | sub: false
117 | },
118 | validate: (artifacts, request, h) => {
119 |
120 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
121 | }
122 | });
123 |
124 | server.auth.default('jwt');
125 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
126 |
127 | await server.initialize();
128 |
129 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, jwks.key.private, { header: { kid: jwks.kid } });
130 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
131 | expect(res.result).to.equal('steve');
132 |
133 | await jwks.server.stop();
134 | });
135 |
136 | it('support headless tokens (string)', async () => {
137 |
138 | const secret = 'some_shared_secret';
139 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret);
140 |
141 | const server = Hapi.server();
142 | await server.register(Jwt);
143 |
144 | server.auth.strategy('jwt', 'jwt', {
145 | keys: secret,
146 | headless: token.split('.')[0],
147 | verify: {
148 | aud: 'urn:audience:test',
149 | iss: 'urn:issuer:test',
150 | sub: false
151 | },
152 | validate: (artifacts, request, h) => {
153 |
154 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
155 | }
156 | });
157 |
158 | server.auth.default('jwt');
159 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
160 |
161 | const headers = {
162 | authorization: `Bearer ${token.slice(token.split('.')[0].length + 1)}`
163 | };
164 |
165 | const res = await server.inject({ url: '/', headers });
166 | expect(res.result).to.equal('steve');
167 | });
168 |
169 | it('support headless tokens (object)', async () => {
170 |
171 | const secret = 'some_shared_secret';
172 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { headless: true });
173 |
174 | const server = Hapi.server();
175 | await server.register(Jwt);
176 |
177 | server.auth.strategy('jwt', 'jwt', {
178 | keys: secret,
179 | headless: { alg: 'HS256', typ: 'JWT' },
180 | verify: {
181 | aud: 'urn:audience:test',
182 | iss: 'urn:issuer:test',
183 | sub: false
184 | },
185 | validate: (artifacts, request, h) => {
186 |
187 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
188 | }
189 | });
190 |
191 | server.auth.default('jwt');
192 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
193 |
194 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
195 | expect(res.result).to.equal('steve');
196 | });
197 |
198 | it('handles failure when headless is defined and token contains header', async () => {
199 |
200 | const secret = 'some_shared_secret';
201 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret);
202 |
203 | const server = Hapi.server();
204 | await server.register(Jwt);
205 |
206 | server.auth.strategy('jwt', 'jwt', {
207 | keys: secret,
208 | headless: { alg: 'HS256', typ: 'JWT' },
209 | verify: {
210 | aud: 'urn:audience:test',
211 | iss: 'urn:issuer:test',
212 | sub: false
213 | },
214 | validate: (artifacts, request, h) => {
215 |
216 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
217 | }
218 | });
219 |
220 | server.auth.default('jwt');
221 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
222 |
223 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
224 | expect(res.statusCode).to.equal(401);
225 | });
226 |
227 | it('support utf-8 (non latin1 characters)', async () => {
228 |
229 | const secret = 'some_shared_secret';
230 | const token = Jwt.token.generate({ user: '史蒂夫', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { location: '图书馆' } });
231 |
232 | const server = Hapi.server();
233 | await server.register(Jwt);
234 |
235 | server.auth.strategy('jwt', 'jwt', {
236 | keys: secret,
237 | verify: {
238 | aud: 'urn:audience:test',
239 | iss: 'urn:issuer:test',
240 | sub: false
241 | },
242 | validate: (artifacts, request, h) => {
243 |
244 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user, location: artifacts.decoded.header.location } };
245 | }
246 | });
247 |
248 | server.auth.default('jwt');
249 | server.route({ path: '/payload', method: 'GET', handler: (request) => request.auth.credentials.user });
250 | server.route({ path: '/header', method: 'GET', handler: (request) => request.auth.credentials.location });
251 |
252 | const res1 = await server.inject({ url: '/payload', headers: { authorization: `Bearer ${token}` } });
253 | expect(res1.result).to.equal('史蒂夫');
254 |
255 | const res2 = await server.inject({ url: '/header', headers: { authorization: `Bearer ${token}` } });
256 | expect(res2.result).to.equal('图书馆');
257 | });
258 |
259 | it('authenticates a request (none)', async () => {
260 |
261 | const server = Hapi.server();
262 | await server.register(Jwt);
263 |
264 | server.auth.strategy('jwt', 'jwt', {
265 | keys: { algorithms: ['none'] },
266 | verify: {
267 | aud: 'urn:audience:test',
268 | iss: 'urn:issuer:test',
269 | sub: false
270 | },
271 | validate: (artifacts, request, h) => {
272 |
273 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
274 | }
275 | });
276 |
277 | server.auth.default('jwt');
278 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
279 |
280 | const token1 = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, '');
281 | const res1 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token1}` } });
282 | expect(res1.result).to.equal('steve');
283 |
284 | const token2 = Jwt.token.generate({ user: 'steve' }, 'some_secret');
285 | const res2 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token2}` } });
286 | expect(res2.statusCode).to.equal(401);
287 | });
288 |
289 | it('supports multiple strategies', async () => {
290 |
291 | const secret = 'some_shared_secret';
292 | const jwks = await Mock.jwks();
293 |
294 | const server = Hapi.server();
295 | await server.register(Jwt);
296 |
297 | server.auth.strategy('secret', 'jwt', {
298 | keys: secret,
299 | verify: {
300 | aud: false,
301 | iss: false,
302 | sub: false
303 | },
304 | validate: false
305 | });
306 |
307 | server.auth.strategy('rsa', 'jwt', {
308 | keys: {
309 | uri: jwks.endpoint
310 | },
311 | verify: {
312 | aud: false,
313 | iss: false,
314 | sub: false
315 | },
316 | validate: false,
317 | httpAuthScheme: 'Other'
318 | });
319 |
320 | server.auth.strategy('rsa2', 'jwt', {
321 | keys: {
322 | uri: jwks.endpoint
323 | },
324 | verify: {
325 | aud: false,
326 | iss: false,
327 | sub: false
328 | },
329 | validate: false,
330 | httpAuthScheme: 'Also'
331 | });
332 |
333 | const handler = (request) => request.auth.credentials.user;
334 | server.route({ path: '/secret', method: 'GET', options: { auth: 'secret', handler } });
335 | server.route({ path: '/rsa', method: 'GET', options: { auth: 'rsa', handler } });
336 | server.route({ path: '/rsa2', method: 'GET', options: { auth: 'rsa2', handler } });
337 | server.route({ path: '/', method: 'GET', options: { auth: { strategies: ['secret', 'rsa', 'rsa2'] }, handler } });
338 |
339 | await server.initialize();
340 |
341 | const token1 = Jwt.token.generate({ user: 'steve' }, secret);
342 | const res1 = await server.inject({ url: '/secret', headers: { authorization: `Bearer ${token1}` } });
343 | expect(res1.result).to.equal('steve');
344 |
345 | const token2 = Jwt.token.generate({ user: 'steve' }, jwks.key.private, { header: { kid: jwks.kid } });
346 | const res2 = await server.inject({ url: '/rsa', headers: { authorization: `Other ${token2}` } });
347 | expect(res2.result).to.equal('steve');
348 |
349 | const token3 = Jwt.token.generate({ user: 'steve' }, jwks.key.private, { header: { kid: jwks.kid } });
350 | const res3 = await server.inject({ url: '/rsa2', headers: { authorization: `Also ${token3}` } });
351 | expect(res3.result).to.equal('steve');
352 |
353 | const res4 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token1}` } });
354 | expect(res4.result).to.equal('steve');
355 |
356 | const res5 = await server.inject({ url: '/', headers: { authorization: `Other ${token2}` } });
357 | expect(res5.result).to.equal('steve');
358 |
359 | const res6 = await server.inject({ url: '/', headers: { authorization: `Also ${token3}` } });
360 | expect(res6.result).to.equal('steve');
361 | });
362 |
363 | it('handles failed validate', async () => {
364 |
365 | const secret = 'some_shared_secret';
366 |
367 | const server = Hapi.server();
368 | await server.register(Jwt);
369 |
370 | server.auth.strategy('jwt', 'jwt', {
371 | keys: secret,
372 | verify: {
373 | aud: false,
374 | iss: false,
375 | sub: false
376 | },
377 | validate: (artifacts, request, h) => {
378 |
379 | if (artifacts.decoded.payload.x === 1) {
380 | return { isValid: false };
381 | }
382 |
383 | if (artifacts.decoded.payload.x === 2) {
384 | throw new Error('oops');
385 | }
386 |
387 | return { response: 'hi!' };
388 | }
389 | });
390 |
391 | server.auth.default('jwt');
392 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
393 |
394 | const token1 = Jwt.token.generate({ x: 1 }, secret);
395 | const res1 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token1}` } });
396 | expect(res1.statusCode).to.equal(401);
397 |
398 | const token2 = Jwt.token.generate({ x: 2 }, secret);
399 | const res2 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token2}` } });
400 | expect(res2.statusCode).to.equal(401);
401 |
402 | const token3 = Jwt.token.generate({ x: 3 }, secret);
403 | const res3 = await server.inject({ url: '/', headers: { authorization: `Bearer ${token3}` } });
404 | expect(res3.statusCode).to.equal(200);
405 | expect(res3.result).to.equal('hi!');
406 | });
407 |
408 | it('skips verify', async () => {
409 |
410 | const secret = 'some_shared_secret';
411 |
412 | const server = Hapi.server();
413 | await server.register(Jwt);
414 |
415 | server.auth.strategy('jwt', 'jwt', {
416 | keys: secret,
417 | verify: false,
418 | validate: (artifacts, request, h) => {
419 |
420 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
421 | }
422 | });
423 |
424 | server.auth.default('jwt');
425 |
426 | const handler = async (request, h) => {
427 |
428 | await server.auth.verify(request);
429 | return request.auth.credentials.user;
430 | };
431 |
432 | server.route({ path: '/', method: 'GET', handler });
433 |
434 | const token = Jwt.token.generate({ user: 'steve' }, 'other_secret');
435 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
436 | expect(res.result).to.equal('steve');
437 | });
438 |
439 | it('reverifies token', { timeout: 4000 }, async () => {
440 |
441 | const secret = 'some_shared_secret';
442 |
443 | const server = Hapi.server();
444 | await server.register(Jwt);
445 |
446 | server.auth.strategy('jwt', 'jwt', {
447 | keys: secret,
448 | verify: {
449 | aud: false,
450 | iss: false,
451 | sub: false
452 | },
453 | validate: false
454 | });
455 |
456 | server.auth.default('jwt');
457 |
458 | const handler = async (request, h) => {
459 |
460 | await server.auth.verify(request);
461 | await Hoek.wait(3000);
462 | try {
463 | await server.auth.verify(request);
464 | }
465 | catch (err) {
466 | return 'ok';
467 | }
468 | };
469 |
470 | server.route({ path: '/', method: 'GET', handler });
471 |
472 | const nowSec = Math.ceil(Date.now() / 1000);
473 | const token = Jwt.token.generate({ user: 'steve', exp: nowSec + 1 }, secret);
474 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
475 | expect(res.result).to.equal('ok');
476 | });
477 |
478 | it('errors on cache warmup error', async () => {
479 |
480 | const jwks = await Mock.jwks({ algorithm: 'RS512' });
481 |
482 | const server = Hapi.server();
483 | await server.register(Jwt);
484 |
485 | server.auth.strategy('jwt', 'jwt', {
486 | keys: {
487 | uri: jwks.endpoint,
488 | algorithms: ['RS256'] // Does not match - always ignored
489 | },
490 | verify: {
491 | aud: false,
492 | iss: false,
493 | sub: false
494 | },
495 | validate: false
496 | });
497 |
498 | await expect(server.initialize()).to.reject('JWKS endpoint response contained no valid keys');
499 | await jwks.server.stop();
500 | });
501 |
502 | it('errors on uninitialized server', async () => {
503 |
504 | const jwks = await Mock.jwks();
505 |
506 | const server = Hapi.server();
507 | await server.register(Jwt);
508 |
509 | server.auth.strategy('jwt', 'jwt', {
510 | keys: {
511 | uri: jwks.endpoint
512 | },
513 | verify: {
514 | aud: false,
515 | iss: false,
516 | sub: false
517 | },
518 | validate: false
519 | });
520 |
521 | server.auth.default('jwt');
522 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
523 |
524 | const token = Jwt.token.generate({ user: 'steve' }, jwks.key.private, { header: { kid: jwks.kid } });
525 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
526 | expect(res.statusCode).to.equal(500);
527 |
528 | await jwks.server.stop();
529 | });
530 |
531 | it('uses authorization as headerName when cookieName or headerName is not specified in config', async () => {
532 |
533 | const secret = 'some_shared_secret';
534 |
535 | const server = Hapi.server();
536 |
537 | server.register(Jwt);
538 |
539 | server.auth.strategy('jwt', 'jwt', {
540 | keys: secret,
541 | verify: {
542 | aud: 'urn:audience:test',
543 | iss: 'urn:issuer:test',
544 | sub: false
545 | },
546 | validate: (artifacts, request, h) => {
547 |
548 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
549 | }
550 | });
551 |
552 | server.auth.default('jwt');
553 |
554 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
555 |
556 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
557 | const res = await server.inject({ url: '/', headers: { authorization: `Bearer ${token}` } });
558 | expect(res.result).to.equal('steve');
559 | });
560 |
561 | it('reads token from cookie specified in cookieName and authenticates', async () => {
562 |
563 | const secret = 'some_shared_secret';
564 |
565 | const server = Hapi.server();
566 |
567 | server.register(Jwt);
568 |
569 | const cookieName = 'random-cookie';
570 |
571 | server.auth.strategy('jwt', 'jwt', {
572 | keys: secret,
573 | verify: {
574 | aud: 'urn:audience:test',
575 | iss: 'urn:issuer:test',
576 | sub: false
577 | },
578 | validate: (artifacts, request, h) => {
579 |
580 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
581 | },
582 | cookieName
583 | });
584 |
585 | server.auth.default('jwt');
586 |
587 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
588 |
589 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
590 | const res = await server.inject({ url: '/', headers: { cookie: `${cookieName}=${token}` } });
591 |
592 | expect(res.result).to.equal('steve');
593 | });
594 |
595 | it('reads token from header specified by headerName and authenticates', async () => {
596 |
597 | const secret = 'some_shared_secret';
598 |
599 | const server = Hapi.server();
600 |
601 | server.register(Jwt);
602 |
603 | const headerName = 'random-header';
604 |
605 | server.auth.strategy('jwt', 'jwt', {
606 | keys: secret,
607 | verify: {
608 | aud: 'urn:audience:test',
609 | iss: 'urn:issuer:test',
610 | sub: false
611 | },
612 | validate: (artifacts, request, h) => {
613 |
614 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
615 | },
616 | headerName
617 | });
618 |
619 | server.auth.default('jwt');
620 |
621 | server.route({ path: '/', method: 'GET', handler: (request) => request.auth.credentials.user });
622 |
623 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
624 | const res = await server.inject({ url: '/', headers: { [headerName]: `Bearer ${token}` } });
625 |
626 | expect(res.result).to.equal('steve');
627 | });
628 |
629 | it('errors when token is not present at headerName or cookieName', async () => {
630 |
631 | const secret = 'some_shared_secret';
632 |
633 | const server = Hapi.server();
634 |
635 | server.register(Jwt);
636 |
637 | const headerName = 'random-header';
638 | const cookieName = 'random-cookie';
639 |
640 | server.auth.strategy('jwt-header', 'jwt', {
641 | keys: secret,
642 | verify: {
643 | aud: 'urn:audience:test',
644 | iss: 'urn:issuer:test',
645 | sub: false
646 | },
647 | validate: (artifacts, request, h) => {
648 |
649 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
650 | },
651 | headerName
652 | });
653 |
654 | server.auth.strategy('jwt-cookie', 'jwt', {
655 | keys: secret,
656 | verify: {
657 | aud: 'urn:audience:test',
658 | iss: 'urn:issuer:test',
659 | sub: false
660 | },
661 | validate: (artifacts, request, h) => {
662 |
663 | return { isValid: true, credentials: { user: artifacts.decoded.payload.user } };
664 | },
665 | cookieName
666 | });
667 |
668 | server.route({
669 | path: '/',
670 | method: 'GET',
671 | options: {
672 | auth: {
673 | strategies: ['jwt-header', 'jwt-cookie']
674 | }
675 | },
676 | handler: (request) => request.auth.credentials.user
677 | });
678 |
679 | const token = Jwt.token.generate({ user: 'steve', aud: 'urn:audience:test', iss: 'urn:issuer:test' }, secret, { header: { kid: 'some' } });
680 |
681 | const resWithHeader = await server.inject({ url: '/', headers: { 'another-header-name': `Bearer ${token}` } });
682 | expect(resWithHeader.statusCode).to.equal(401);
683 |
684 | const resWithCookie = await server.inject({ url: '/', headers: { 'cookie': `another-cookie=${token}` } });
685 | expect(resWithCookie.statusCode).to.equal(401);
686 | });
687 | });
688 |
--------------------------------------------------------------------------------
/test/token.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Hoek = require('@hapi/hoek');
5 | const Jwt = require('..');
6 | const Lab = require('@hapi/lab');
7 |
8 | const Mock = require('./mock');
9 |
10 |
11 | const internals = {};
12 |
13 |
14 | const { describe, it } = exports.lab = Lab.script();
15 | const expect = Code.expect;
16 |
17 |
18 | describe('Token', () => {
19 |
20 | it('creates and verifies a token', () => {
21 |
22 | const secret = 'some_shared_secret';
23 | const token = Jwt.token.generate({ test: 'ok' }, secret);
24 | const artifacts = Jwt.token.decode(token);
25 | Jwt.token.verify(artifacts, secret);
26 |
27 | expect(artifacts.decoded).to.equal({
28 | header: { alg: 'HS256', typ: 'JWT' },
29 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
30 | signature: artifacts.decoded.signature
31 | });
32 | });
33 |
34 | it('creates and verifies a token (HS512)', () => {
35 |
36 | const secret = 'some_shared_secret';
37 | const token = Jwt.token.generate({ test: 'ok' }, { key: secret, algorithm: 'HS512' });
38 | const artifacts = Jwt.token.decode(token);
39 | Jwt.token.verify(artifacts, secret);
40 |
41 | expect(artifacts.decoded).to.equal({
42 | header: { alg: 'HS512', typ: 'JWT' },
43 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
44 | signature: artifacts.decoded.signature
45 | });
46 | });
47 |
48 | it('creates and verifies a token (EdDSA,ed25519)', () => {
49 |
50 | const pair = Mock.pair('EdDSA', 'ed25519');
51 | const token = Jwt.token.generate({ test: 'ok' }, { key: pair.private, algorithm: 'EdDSA' });
52 |
53 | const artifacts = Jwt.token.decode(token);
54 | Jwt.token.verify(artifacts, pair.public);
55 |
56 |
57 | expect(artifacts.decoded).to.equal({
58 | header: { alg: 'EdDSA', typ: 'JWT' },
59 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
60 | signature: artifacts.decoded.signature
61 | });
62 | });
63 |
64 | it('creates and verifies a token (EdDSA,ed448)', () => {
65 |
66 | const pair = Mock.pair('EdDSA', 'ed448');
67 | const token = Jwt.token.generate({ test: 'ok' }, { key: pair.private, algorithm: 'EdDSA' });
68 |
69 | const artifacts = Jwt.token.decode(token);
70 | Jwt.token.verify(artifacts, pair.public);
71 |
72 |
73 | expect(artifacts.decoded).to.equal({
74 | header: { alg: 'EdDSA', typ: 'JWT' },
75 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
76 | signature: artifacts.decoded.signature
77 | });
78 | });
79 |
80 | it('creates and verifies a token (none)', () => {
81 |
82 | const token = Jwt.token.generate({ test: 'ok' }, '');
83 | const artifacts = Jwt.token.decode(token);
84 | Jwt.token.verify(artifacts, '');
85 |
86 | expect(artifacts.decoded).to.equal({
87 | header: { alg: 'none', typ: 'JWT' },
88 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
89 | signature: ''
90 | });
91 | });
92 |
93 | it('creates and verifies a token (no typ)', () => {
94 |
95 | const secret = 'some_shared_secret';
96 | const token = Jwt.token.generate({ test: 'ok' }, secret, { typ: false });
97 | const artifacts = Jwt.token.decode(token);
98 | Jwt.token.verify(artifacts, secret);
99 |
100 | expect(artifacts.decoded).to.equal({
101 | header: { alg: 'HS256' },
102 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
103 | signature: artifacts.decoded.signature
104 | });
105 | });
106 |
107 | it('creates and verifies a headless token', () => {
108 |
109 | const secret = 'some_shared_secret';
110 | const token = Jwt.token.generate({ test: 'ok' }, secret, { headless: true });
111 | const artifacts = Jwt.token.decode(token, { headless: { alg: 'HS256', typ: 'JWT' } });
112 |
113 | expect(artifacts.decoded).to.equal({
114 | header: { alg: 'HS256', typ: 'JWT' },
115 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
116 | signature: artifacts.decoded.signature
117 | });
118 | });
119 |
120 | describe('generate()', () => {
121 |
122 | it('creates and verifies a token (custom now)', () => {
123 |
124 | const secret = 'some_shared_secret';
125 | const token = Jwt.token.generate({ test: 'ok' }, { key: secret }, { now: 1556520613637 });
126 | const artifacts = Jwt.token.decode(token);
127 | Jwt.token.verify(artifacts, secret);
128 |
129 | expect(artifacts.decoded).to.equal({
130 | header: { alg: 'HS256', typ: 'JWT' },
131 | payload: { test: 'ok', iat: 1556520613 },
132 | signature: 'YjUQB7jHyZyarkUZe0Lx4vngqZaIQTZU24k71jJVHBo'
133 | });
134 | });
135 |
136 | it('creates and verifies a token (custom iat)', () => {
137 |
138 | const secret = 'some_shared_secret';
139 | const token = Jwt.token.generate({ test: 'ok', iat: 1556520613 }, { key: secret });
140 | const artifacts = Jwt.token.decode(token);
141 | Jwt.token.verify(artifacts, secret);
142 |
143 | expect(artifacts.decoded).to.equal({
144 | header: { alg: 'HS256', typ: 'JWT' },
145 | payload: { test: 'ok', iat: 1556520613 },
146 | signature: 'YjUQB7jHyZyarkUZe0Lx4vngqZaIQTZU24k71jJVHBo'
147 | });
148 | });
149 |
150 | it('creates and verifies a token (custom header fields)', () => {
151 |
152 | const secret = 'some_shared_secret';
153 | const token = Jwt.token.generate({ test: 'ok' }, { key: secret }, { now: 1556520613637, header: { extra: 'value' } });
154 | const artifacts = Jwt.token.decode(token);
155 | Jwt.token.verify(artifacts, secret);
156 |
157 | expect(artifacts.decoded).to.equal({
158 | header: { alg: 'HS256', typ: 'JWT', extra: 'value' },
159 | payload: { test: 'ok', iat: 1556520613 },
160 | signature: 'm1cWB5oNHM_ygxoN6eVlZPBKq5ysyJ9vR8e7ikM0gBU'
161 | });
162 | });
163 |
164 | it('creates token with ttl', async () => {
165 |
166 | const secret = 'some_shared_secret';
167 | const token = Jwt.token.generate({ test: 'ok' }, { key: secret }, { ttlSec: 1 });
168 | const artifacts = Jwt.token.decode(token);
169 | Jwt.token.verify(artifacts, secret);
170 |
171 | await Hoek.wait(1000);
172 |
173 | expect(() => Jwt.token.verify(artifacts, secret)).to.throw('Token expired');
174 | });
175 |
176 | it('ignores ttl when exp present', () => {
177 |
178 | const secret = 'some_shared_secret';
179 | const token = Jwt.token.generate({ test: 'ok', exp: 123 }, { key: secret }, { ttlSec: 1 });
180 | const artifacts = Jwt.token.decode(token);
181 | expect(artifacts.decoded.payload.exp).to.equal(123);
182 | });
183 | });
184 |
185 | describe('decode()', () => {
186 |
187 | it('decodes a headless token (encoded)', () => {
188 |
189 | const secret = 'some_shared_secret';
190 | const token = Jwt.token.generate({ test: 'ok' }, secret);
191 | const head = token.split('.', 1)[0];
192 | const tail = token.substring(head.length + 1);
193 | const artifacts = Jwt.token.decode(tail, { headless: head });
194 | Jwt.token.verify(artifacts, secret);
195 |
196 | expect(artifacts.decoded).to.equal({
197 | header: { alg: 'HS256', typ: 'JWT' },
198 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
199 | signature: artifacts.decoded.signature
200 | });
201 | });
202 |
203 | it('decodes a headless token (object)', () => {
204 |
205 | const secret = 'some_shared_secret';
206 | const token = Jwt.token.generate({ test: 'ok' }, secret);
207 | const orig = Jwt.token.decode(token);
208 |
209 | const artifacts = Jwt.token.decode(orig.raw.payload + '.' + orig.raw.signature, { headless: orig.decoded.header });
210 | Jwt.token.verify(artifacts, secret);
211 |
212 | expect(artifacts.decoded).to.equal({
213 | header: { alg: 'HS256', typ: 'JWT' },
214 | payload: { test: 'ok', iat: artifacts.decoded.payload.iat },
215 | signature: artifacts.decoded.signature
216 | });
217 | });
218 |
219 | it('errors on incorrect number of parts', () => {
220 |
221 | expect(() => Jwt.token.decode('a')).to.throw('Invalid token structure');
222 | expect(() => Jwt.token.decode('a.b')).to.throw('Invalid token structure');
223 | });
224 |
225 | it('errors on invalid parts', () => {
226 |
227 | expect(() => Jwt.token.decode('@.abc.def')).to.throw('Invalid token header part');
228 | expect(() => Jwt.token.decode('abc.@.def')).to.throw('Invalid token payload part');
229 | expect(() => Jwt.token.decode('abc.def.@')).to.throw('Invalid token signature part');
230 | });
231 |
232 | it('errors on missing header', () => {
233 |
234 | expect(() => Jwt.token.decode('.a.b')).to.throw('Invalid token missing header');
235 | });
236 |
237 | it('errors on invalid payload', () => {
238 |
239 | expect(() => Jwt.token.decode(`${Jwt.utils.b64stringify({ typ: 'JWT', alg: 'none' })}.xxx.`)).to.throw('Invalid token payload');
240 | expect(() => Jwt.token.decode(`${Jwt.utils.b64stringify({ typ: 'JWT', alg: 'none' })}.${Jwt.utils.b64stringify(123)}.`)).to.throw('Invalid token payload');
241 | expect(() => Jwt.token.decode(`${Jwt.utils.b64stringify({ typ: 'JWT', alg: 'none' })}.${Jwt.utils.b64stringify([])}.`)).to.throw('Invalid token payload');
242 | });
243 |
244 | it('errors on missing algorithm', () => {
245 |
246 | const token = `${Jwt.utils.b64stringify({ typ: 'JWT' })}.${Jwt.utils.b64stringify({}, 'utf8')}.`;
247 | expect(() => Jwt.token.decode(token)).to.throw('Token header missing alg attribute');
248 | });
249 |
250 | it('errors on header present in token and headless provided', () => {
251 |
252 | const secret = 'some_shared_secret';
253 | const token = Jwt.token.generate({ test: 'ok' }, secret);
254 | expect(() => Jwt.token.decode(token, { headless: { alg: 'HS256', typ: 'JWT' } })).to.throw('Token contains header');
255 | });
256 | });
257 |
258 | describe('verifySignature()', () => {
259 |
260 | it('validates signature', () => {
261 |
262 | const secret = 'some_shared_secret';
263 | const token = Jwt.token.generate({ test: 'ok' }, secret);
264 | const artifacts = Jwt.token.decode(token);
265 |
266 | Jwt.token.verifySignature(artifacts, { key: secret, algorithm: 'HS256' });
267 | Jwt.token.verifySignature(artifacts, { key: secret, algorithms: ['HS256', 'HS512'] });
268 | });
269 |
270 | it('invalidates signature', () => {
271 |
272 | const secret = 'some_shared_secret';
273 | const token = Jwt.token.generate({ test: 'ok' }, secret);
274 | const artifacts = Jwt.token.decode(token);
275 |
276 | Jwt.token.verify(artifacts, secret);
277 |
278 | expect(() => Jwt.token.verify(artifacts, 'wrong_secret')).to.throw('Invalid token signature');
279 | });
280 | });
281 |
282 | describe('verifyPayload()', () => {
283 |
284 | it('validates claims', () => {
285 |
286 | const payload = {
287 | iss: 'urn:example',
288 | sub: 'urn:subject',
289 | jti: 'abc',
290 | nonce: 'xyz'
291 | };
292 |
293 | const token = Jwt.token.generate(payload, '');
294 | const artifacts = Jwt.token.decode(token);
295 |
296 | expect(() => Jwt.token.verify(artifacts, '')).to.not.throw();
297 | expect(() => Jwt.token.verify(artifacts, '', { iss: payload.iss })).to.not.throw();
298 | expect(() => Jwt.token.verify(artifacts, '', { iss: [payload.iss] })).to.not.throw();
299 | expect(() => Jwt.token.verify(artifacts, '', { sub: payload.sub })).to.not.throw();
300 | expect(() => Jwt.token.verify(artifacts, '', { jti: payload.jti })).to.not.throw();
301 | expect(() => Jwt.token.verify(artifacts, '', { nonce: payload.nonce })).to.not.throw();
302 |
303 | expect(() => Jwt.token.verify(artifacts, '', { iss: 'wrong' })).to.throw('Token payload iss value not allowed');
304 | expect(() => Jwt.token.verify(artifacts, '', { iss: ['wrong'] })).to.throw('Token payload iss value not allowed');
305 | expect(() => Jwt.token.verify(artifacts, '', { sub: 'wrong' })).to.throw('Token payload sub value not allowed');
306 | expect(() => Jwt.token.verify(artifacts, '', { jti: 'wrong' })).to.throw('Token payload jti value not allowed');
307 | expect(() => Jwt.token.verify(artifacts, '', { nonce: 'wrong' })).to.throw('Token payload nonce value not allowed');
308 | });
309 |
310 | it('validates missing claims', () => {
311 |
312 | const token = Jwt.token.generate({}, '');
313 | const artifacts = Jwt.token.decode(token);
314 |
315 | expect(() => Jwt.token.verify(artifacts, '')).to.not.throw();
316 |
317 | expect(() => Jwt.token.verify(artifacts, '', { iss: 'a' })).to.throw('Token missing payload iss value');
318 | expect(() => Jwt.token.verify(artifacts, '', { sub: 'a' })).to.throw('Token missing payload sub value');
319 | expect(() => Jwt.token.verify(artifacts, '', { jti: 'a' })).to.throw('Token missing payload jti value');
320 | expect(() => Jwt.token.verify(artifacts, '', { nonce: 'a' })).to.throw('Token missing payload nonce value');
321 | });
322 |
323 | it('validates audience', () => {
324 |
325 | const payload = {
326 | aud: 'a1'
327 | };
328 |
329 | const token = Jwt.token.generate(payload, '');
330 | const artifacts = Jwt.token.decode(token);
331 |
332 | expect(() => Jwt.token.verify(artifacts, '')).to.not.throw();
333 | expect(() => Jwt.token.verify(artifacts, '', { aud: 'a1' })).to.not.throw();
334 | expect(() => Jwt.token.verify(artifacts, '', { aud: ['a1'] })).to.not.throw();
335 | expect(() => Jwt.token.verify(artifacts, '', { aud: ['a1', 'b1'] })).to.not.throw();
336 | expect(() => Jwt.token.verify(artifacts, '', { aud: /a1/ })).to.not.throw();
337 | expect(() => Jwt.token.verify(artifacts, '', { aud: [/a1/] })).to.not.throw();
338 |
339 | expect(() => Jwt.token.verify(artifacts, '', { aud: 'wrong' })).to.throw('Token audience is not allowed');
340 | expect(() => Jwt.token.verify(artifacts, '', { aud: ['wrong'] })).to.throw('Token audience is not allowed');
341 | expect(() => Jwt.token.verify(artifacts, '', { aud: /wrong/ })).to.throw('Token audience is not allowed');
342 | });
343 |
344 | it('validates audiences', () => {
345 |
346 | const payload = {
347 | aud: ['a1', 'b1']
348 | };
349 |
350 | const token = Jwt.token.generate(payload, '');
351 | const artifacts = Jwt.token.decode(token);
352 |
353 | expect(() => Jwt.token.verify(artifacts, '')).to.not.throw();
354 | expect(() => Jwt.token.verify(artifacts, '', { aud: 'a1' })).to.not.throw();
355 | expect(() => Jwt.token.verify(artifacts, '', { aud: ['a1'] })).to.not.throw();
356 | expect(() => Jwt.token.verify(artifacts, '', { aud: 'b1' })).to.not.throw();
357 |
358 | expect(() => Jwt.token.verify(artifacts, '', { aud: 'wrong' })).to.throw('Token audience is not allowed');
359 | expect(() => Jwt.token.verify(artifacts, '', { aud: ['wrong'] })).to.throw('Token audience is not allowed');
360 | });
361 |
362 | it('errors on missing audience', () => {
363 |
364 | const token = Jwt.token.generate({}, '');
365 | const artifacts = Jwt.token.decode(token);
366 |
367 | expect(() => Jwt.token.verify(artifacts, '', { aud: ['wrong'] })).to.throw('Token missing payload aud value');
368 | });
369 |
370 | it('validates expiration', () => {
371 |
372 | const now = 1556582767980;
373 |
374 | const payload = {
375 | exp: 1556582770
376 | };
377 |
378 | const token = Jwt.token.generate(payload, '');
379 | const artifacts = Jwt.token.decode(token);
380 |
381 | expect(() => Jwt.token.verify(artifacts, '', { exp: false })).to.not.throw();
382 | expect(() => Jwt.token.verify(artifacts, '', { now })).to.not.throw();
383 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000, timeSkewSec: 15 })).to.not.throw();
384 |
385 | expect(() => Jwt.token.verify(artifacts, '')).to.throw('Token expired');
386 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000 })).to.throw('Token expired');
387 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000, timeSkewSec: 1 })).to.throw('Token expired');
388 | });
389 |
390 | it('errors on invalid expiration', () => {
391 |
392 | const payload = {
393 | exp: '123'
394 | };
395 |
396 | const token = Jwt.token.generate(payload, '');
397 | const artifacts = Jwt.token.decode(token);
398 |
399 | expect(() => Jwt.token.verify(artifacts, '')).to.throw('Invalid payload exp value');
400 | });
401 |
402 | it('validates max age', () => {
403 |
404 | const now = 1556582767980;
405 |
406 | const payload = {
407 | iat: 1556582767
408 | };
409 |
410 | const token = Jwt.token.generate(payload, '');
411 | const artifacts = Jwt.token.decode(token);
412 |
413 | expect(() => Jwt.token.verify(artifacts, '', { iat: false })).to.not.throw();
414 | expect(() => Jwt.token.verify(artifacts, '', { now })).to.not.throw();
415 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000, maxAgeSec: 10 })).to.not.throw();
416 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000, timeSkewSec: 15, maxAgeSec: 5 })).to.not.throw();
417 |
418 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000, maxAgeSec: 5 })).to.throw('Token maximum age exceeded');
419 | expect(() => Jwt.token.verify(artifacts, '', { now: now + 10000, maxAgeSec: 5, timeSkewSec: 1 })).to.throw('Token maximum age exceeded');
420 | });
421 |
422 | it('errors on invalid iat', () => {
423 |
424 | const token = Jwt.token.generate({ iat: '123' }, '');
425 | const artifacts = Jwt.token.decode(token);
426 |
427 | expect(() => Jwt.token.verify(artifacts, '', { maxAgeSec: 1000 })).to.throw('Missing or invalid payload iat value');
428 | });
429 |
430 | it('errors on null iat', () => {
431 |
432 | const token = Jwt.token.generate({ iat: null }, '');
433 | const artifacts = Jwt.token.decode(token);
434 |
435 | expect(() => Jwt.token.verify(artifacts, '', { maxAgeSec: 1000 })).to.throw('Missing or invalid payload iat value');
436 | });
437 |
438 | it('errors on missing iat', () => {
439 |
440 | const token = Jwt.token.generate({}, '', { iat: false });
441 | const artifacts = Jwt.token.decode(token);
442 |
443 | expect(() => Jwt.token.verify(artifacts, '', { maxAgeSec: 1000 })).to.throw('Missing or invalid payload iat value');
444 | });
445 |
446 | it('validates not before', () => {
447 |
448 | const now = 1556582767980;
449 |
450 | const payload = {
451 | nbf: 1556582777
452 | };
453 |
454 | const token = Jwt.token.generate(payload, '');
455 | const artifacts = Jwt.token.decode(token);
456 |
457 | expect(() => Jwt.token.verify(artifacts, '')).to.not.throw();
458 | expect(() => Jwt.token.verify(artifacts, '', { now, nbf: false })).to.not.throw();
459 | expect(() => Jwt.token.verify(artifacts, '', { now, timeSkewSec: 15 })).to.not.throw();
460 |
461 | expect(() => Jwt.token.verify(artifacts, '', { now })).to.throw('Token not yet active');
462 | expect(() => Jwt.token.verify(artifacts, '', { now, timeSkewSec: 1 })).to.throw('Token not yet active');
463 | });
464 |
465 | it('errors on invalid nbf', () => {
466 |
467 | const token = Jwt.token.generate({ nbf: '123' }, '');
468 | const artifacts = Jwt.token.decode(token);
469 |
470 | expect(() => Jwt.token.verify(artifacts, '')).to.throw('Invalid payload nbf value');
471 | });
472 | });
473 |
474 | describe('verifyTime()', () => {
475 |
476 | it('validates expiration', () => {
477 |
478 | const now = 1556582767980;
479 |
480 | const payload = {
481 | exp: 1556582770
482 | };
483 |
484 | const token = Jwt.token.generate(payload, '');
485 | const artifacts = Jwt.token.decode(token);
486 |
487 | expect(() => Jwt.token.verifyTime(artifacts, { exp: false })).to.not.throw();
488 | expect(() => Jwt.token.verifyTime(artifacts, { now })).to.not.throw();
489 | expect(() => Jwt.token.verifyTime(artifacts, { now: now + 10000, timeSkewSec: 15 })).to.not.throw();
490 |
491 | expect(() => Jwt.token.verifyTime(artifacts)).to.throw('Token expired');
492 | expect(() => Jwt.token.verifyTime(artifacts, { now: now + 10000 })).to.throw('Token expired');
493 | expect(() => Jwt.token.verifyTime(artifacts, { now: now + 10000, timeSkewSec: 1 })).to.throw('Token expired');
494 | });
495 | });
496 |
497 | describe('key()', () => {
498 |
499 | it('errors on incompatible key', () => {
500 |
501 | const secret = 'some_shared_secret';
502 | const token = Jwt.token.generate({ test: 'ok' }, secret);
503 | const artifacts = Jwt.token.decode(token);
504 | expect(() => Jwt.token.verify(artifacts, Mock.pair().public)).to.throw('Unsupported algorithm');
505 | });
506 | });
507 | });
508 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Code = require('@hapi/code');
4 | const Jwt = require('..');
5 | const Lab = require('@hapi/lab');
6 |
7 |
8 | const internals = {};
9 |
10 |
11 | const { describe, it } = exports.lab = Lab.script();
12 | const expect = Code.expect;
13 |
14 |
15 | describe('Utils', () => {
16 |
17 | describe('toHex()', () => {
18 |
19 | it('converts numbers to hex string', () => {
20 |
21 | expect(Jwt.utils.toHex(1)).to.equal('01');
22 | expect(Jwt.utils.toHex(100001)).to.equal('0186a1');
23 | expect(Jwt.utils.toHex(4040404)).to.equal('3da6d4');
24 | });
25 | });
26 |
27 | describe('validHttpTokenSchema', () => {
28 |
29 | it('allows valid http tokens', () => {
30 |
31 | const validHttpTokens = ['cookie-name', 'cookie_name', '_cookie-name', 'cookie__name', 'cookie--name', '__--cookie--name__--'];
32 |
33 | for (const token of validHttpTokens) {
34 | expect(Jwt.utils.validHttpTokenSchema.validate(token).error).to.not.exist();
35 | }
36 | });
37 |
38 | it('errors for invalid http tokens', () => {
39 |
40 | const invalidHttpTokens = [
41 | 'a)b', 'a(b', 'ab', 'a@b', 'a,b', 'a;b', 'a:b', 'a\\b',
42 | 'a/b', 'a[b', 'a]b', 'a?b', 'a=b', 'q{a', 'b}n'
43 | ];
44 |
45 | for (const token of invalidHttpTokens) {
46 | expect(Jwt.utils.validHttpTokenSchema.validate(token).error).to.exist();
47 | }
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------