@questwork/q-utilities 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/index.js +3 -0
- package/lib/authenticator/authenticator.js +56 -0
- package/lib/authenticator/clientApp.js +46 -0
- package/lib/authenticator/index.js +5 -0
- package/lib/convertString/convertString.js +20 -0
- package/lib/convertString/index.js +1 -0
- package/lib/cryptoHelper/cryptoHelper.js +93 -0
- package/lib/cryptoHelper/index.js +5 -0
- package/lib/getValidation/getValidation.js +117 -0
- package/lib/getValidation/index.js +1 -0
- package/lib/getValueByKeys/getValueByKeys.js +20 -0
- package/lib/getValueByKeys/index.js +1 -0
- package/lib/index.js +14 -0
- package/lib/jwtHelper/index.js +5 -0
- package/lib/jwtHelper/jwtHelper.js +254 -0
- package/lib/keyValueObject/index.js +1 -0
- package/lib/keyValueObject/keyValueObject.js +163 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright 2019 Questwork Consulting Limited
|
|
2
|
+
|
|
3
|
+
This project is free software released under the MIT/X11 license:
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
const { ClientApp } = require('./clientApp')
|
|
3
|
+
|
|
4
|
+
async function getAuthToken({ id, appSecret, name, service, authServerHost, authServerPath, authPath, additionalData }) {
|
|
5
|
+
try {
|
|
6
|
+
const clientApp = _initClientApp({ id, appSecret, name })
|
|
7
|
+
return _getAuthToken({
|
|
8
|
+
clientApp, service, authServerHost, authServerPath, authPath, additionalData
|
|
9
|
+
})
|
|
10
|
+
} catch (err) {
|
|
11
|
+
throw err
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function _getAuthCredentials({ clientApp, service, additionalData }) {
|
|
16
|
+
const jwt = clientApp.getJwtToken()
|
|
17
|
+
const data = {
|
|
18
|
+
service,
|
|
19
|
+
...additionalData
|
|
20
|
+
}
|
|
21
|
+
const headers = { Authorization: `Bearer ${jwt}` }
|
|
22
|
+
return {
|
|
23
|
+
data,
|
|
24
|
+
headers
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function _getAuthToken({ clientApp, service, authServerHost, authServerPath, authPath, additionalData }) {
|
|
29
|
+
try {
|
|
30
|
+
const path = authServerPath || authPath
|
|
31
|
+
if (!!path && !!authServerHost) {
|
|
32
|
+
const { data, headers } = _getAuthCredentials({ clientApp, service, additionalData })
|
|
33
|
+
const response = await axios.post(`${authServerHost}${path}`, data, { headers })
|
|
34
|
+
return response.data
|
|
35
|
+
}
|
|
36
|
+
throw new Error('missing auth server')
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err.response && err.response.data ? err.response.data.message : err.message
|
|
39
|
+
throw new Error(`failed to get ServiceApp token: ${message}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _initClientApp({ id, appSecret, name } = {}) {
|
|
44
|
+
try {
|
|
45
|
+
const clientApp = ClientApp.init({
|
|
46
|
+
id, appSecret, name
|
|
47
|
+
})
|
|
48
|
+
return clientApp
|
|
49
|
+
} catch (err) {
|
|
50
|
+
throw new Error(`invalid ClientApp: ${err.message}`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
getAuthToken
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { jwtHelper } = require('../jwtHelper')
|
|
2
|
+
|
|
3
|
+
const TOKEN_EXPIRY = 300
|
|
4
|
+
|
|
5
|
+
class ClientApp {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
options = options || {}
|
|
8
|
+
this.id = options.id
|
|
9
|
+
this.appSecret = options.appSecret
|
|
10
|
+
this.name = options.name
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Class method
|
|
14
|
+
static init(options = {}) {
|
|
15
|
+
const clientApp = new ClientApp(options)
|
|
16
|
+
return clientApp.isValid ? clientApp : null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Getter
|
|
20
|
+
get isValid() {
|
|
21
|
+
return !!this.name && !!this.appSecret && !!this.id
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// instance methods
|
|
25
|
+
getJwtPayload() {
|
|
26
|
+
return {
|
|
27
|
+
clientApp: {
|
|
28
|
+
id: this.id,
|
|
29
|
+
name: this.name
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
getJwtToken({ tokenExpiry } = {}) {
|
|
34
|
+
const iExpiry = tokenExpiry || Math.floor(Date.now() / 1000) + TOKEN_EXPIRY // must be second
|
|
35
|
+
return jwtHelper.create(
|
|
36
|
+
this.getJwtPayload(),
|
|
37
|
+
{
|
|
38
|
+
secret: this.appSecret,
|
|
39
|
+
expiry: iExpiry
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
ClientApp
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function convertString(string, patternMatch = /\$\{(.+?)\}/g, value, getValueByKeys) {
|
|
2
|
+
if (!string || typeof getValueByKeys !== 'function') {
|
|
3
|
+
return ''
|
|
4
|
+
}
|
|
5
|
+
const reg = new RegExp(patternMatch, 'g')
|
|
6
|
+
return string.replace(reg, (match, key) => {
|
|
7
|
+
const result = getValueByKeys({ keys: key.split('.'), obj: value })
|
|
8
|
+
if (result === null || result === undefined) {
|
|
9
|
+
return ''
|
|
10
|
+
}
|
|
11
|
+
return typeof result === 'object' ? JSON.stringify(result) : result
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
convertString
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
convertString
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { convertString } from './convertString'
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// const crypto = require('crypto-browserify')
|
|
2
|
+
const crypto = require('crypto')
|
|
3
|
+
// const { subtle } = globalThis.crypto
|
|
4
|
+
|
|
5
|
+
const cryptoHelper = {
|
|
6
|
+
createHash(algorithm, options) {
|
|
7
|
+
return crypto.createHash(algorithm, options)
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
createHmac(algorithm, key, options) {
|
|
11
|
+
return crypto.createHmac(algorithm, key, options)
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
createSign(algorithm, options) {
|
|
15
|
+
return crypto.createSign(algorithm, options)
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
createVerify(algorithm, options) {
|
|
19
|
+
return crypto.createVerify(algorithm, options)
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
digest(instance, outputEncoding) {
|
|
23
|
+
if (!(typeof instance.digest === 'function')) {
|
|
24
|
+
throw new Error('no digest method')
|
|
25
|
+
}
|
|
26
|
+
return instance.digest(outputEncoding)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
sign({ instance, privateKey, outputEncoding }) {
|
|
30
|
+
if (!(typeof instance.sign === 'function')) {
|
|
31
|
+
throw new Error('no sign method')
|
|
32
|
+
}
|
|
33
|
+
return instance.sign(privateKey, outputEncoding)
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
verify({ instance, object, signature, signatureEncoding }) {
|
|
37
|
+
if (!(typeof instance.verify === 'function')) {
|
|
38
|
+
throw new Error('no verify method')
|
|
39
|
+
}
|
|
40
|
+
return instance.verify(object, signature, signatureEncoding)
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
update({ instance, data, inputEncoding }) {
|
|
44
|
+
if (!(typeof instance.update === 'function')) {
|
|
45
|
+
throw new Error('no update method')
|
|
46
|
+
}
|
|
47
|
+
return instance.update(data, inputEncoding)
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
createStringHmac({ algorithm, options, key, data, inputEncoding, outputEncoding }) {
|
|
51
|
+
try {
|
|
52
|
+
const hmac = this.createHmac(algorithm, key, options)
|
|
53
|
+
const instance = this.update({ instance: hmac, data, inputEncoding })
|
|
54
|
+
return this.digest(instance, outputEncoding)
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw err
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
createStringVerify({ algorithm, options, data, object, signature, inputEncoding, signatureEncoding }) {
|
|
61
|
+
try {
|
|
62
|
+
const verify = this.createVerify(algorithm, options)
|
|
63
|
+
const instance = this.update({ instance: verify, data, inputEncoding })
|
|
64
|
+
return this.verify({ instance, object, signature, signatureEncoding })
|
|
65
|
+
} catch (err) {
|
|
66
|
+
throw err
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
createSignature({ algorithm, options, data, privateKey, inputEncoding, outputEncoding }) {
|
|
71
|
+
try {
|
|
72
|
+
const sign = this.createSign(algorithm, options)
|
|
73
|
+
const instance = this.update({ instance: sign, data, inputEncoding })
|
|
74
|
+
return this.sign({ instance, privateKey, outputEncoding })
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
createStringHash({ algorithm, options, data, inputEncoding, outputEncoding }) {
|
|
81
|
+
try {
|
|
82
|
+
const hash = this.createHash(algorithm, options)
|
|
83
|
+
const instance = this.update({ instance: hash, data, inputEncoding })
|
|
84
|
+
return this.digest(instance, outputEncoding)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw err
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
cryptoHelper
|
|
93
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
function getValidation(rule, data, getDataByKey, KeyValueObject) {
|
|
2
|
+
if (!rule) {
|
|
3
|
+
return true
|
|
4
|
+
}
|
|
5
|
+
if (typeof getDataByKey !== 'function' || typeof KeyValueObject !== 'function') {
|
|
6
|
+
return false
|
|
7
|
+
}
|
|
8
|
+
const { key = '', value, keyValuePath = '' } = rule
|
|
9
|
+
const [valueAttribute] = Object.keys(value)
|
|
10
|
+
|
|
11
|
+
if (!key) {
|
|
12
|
+
switch (valueAttribute) {
|
|
13
|
+
case '$and': {
|
|
14
|
+
const arr = value['$and']
|
|
15
|
+
return arr.reduce((acc, item) => (acc && getValidation(item, data, getDataByKey)), true)
|
|
16
|
+
}
|
|
17
|
+
case '$or': {
|
|
18
|
+
const arr = value['$or']
|
|
19
|
+
return arr.reduce((acc, item) => (acc || getValidation(item, data, getDataByKey)), false)
|
|
20
|
+
}
|
|
21
|
+
default:
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let rowValue = getDataByKey(key, data)
|
|
27
|
+
|
|
28
|
+
// debugger
|
|
29
|
+
|
|
30
|
+
// if KeyValue object
|
|
31
|
+
if (keyValuePath) {
|
|
32
|
+
console.log('keyValuePath', keyValuePath)
|
|
33
|
+
const rowValueData = KeyValueObject.toObject(rowValue)
|
|
34
|
+
rowValue = getDataByKey(keyValuePath, rowValueData)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
switch (valueAttribute) {
|
|
38
|
+
case '$empty': {
|
|
39
|
+
const isEmpty = rowValue === null || rowValue === undefined
|
|
40
|
+
return isEmpty === value['$empty']
|
|
41
|
+
}
|
|
42
|
+
case '$eq': {
|
|
43
|
+
return rowValue === value['$eq']
|
|
44
|
+
}
|
|
45
|
+
case '$gt': {
|
|
46
|
+
return rowValue > value['$gt']
|
|
47
|
+
}
|
|
48
|
+
case '$gte': {
|
|
49
|
+
return rowValue >= value['$gte']
|
|
50
|
+
}
|
|
51
|
+
case '$lt': {
|
|
52
|
+
return rowValue < value['$lt']
|
|
53
|
+
}
|
|
54
|
+
case '$lte': {
|
|
55
|
+
return rowValue <= value['$lte']
|
|
56
|
+
}
|
|
57
|
+
case '$in': {
|
|
58
|
+
if (Array.isArray(rowValue)) {
|
|
59
|
+
return !!rowValue.find((e) => (value['$in'].includes(e)))
|
|
60
|
+
}
|
|
61
|
+
if (typeof rowValue !== 'object') {
|
|
62
|
+
return !!value['$in'].includes(rowValue)
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
case '$inValue': {
|
|
67
|
+
const result = getDataByKey(value['$inValue'], data)
|
|
68
|
+
const _value = Array.isArray(result) ? result : []
|
|
69
|
+
if (Array.isArray(rowValue)) {
|
|
70
|
+
return !!rowValue.find((e) => (_value.includes(e)))
|
|
71
|
+
}
|
|
72
|
+
if (typeof rowValue === 'string') {
|
|
73
|
+
return !!_value.includes(rowValue)
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
case '$ne': {
|
|
78
|
+
return rowValue !== value['$ne']
|
|
79
|
+
}
|
|
80
|
+
case '$notIn': {
|
|
81
|
+
if (Array.isArray(rowValue)) {
|
|
82
|
+
return !rowValue.find((e) => (value['$notIn'].includes(e)))
|
|
83
|
+
}
|
|
84
|
+
if (typeof rowValue !== 'object') {
|
|
85
|
+
return !value['$notIn'].includes(rowValue)
|
|
86
|
+
}
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
case '$notInValue': {
|
|
90
|
+
const result = getDataByKey(value['$notInValue'], data)
|
|
91
|
+
const _value = Array.isArray(result) ? result : []
|
|
92
|
+
if (Array.isArray(rowValue)) {
|
|
93
|
+
return !rowValue.find((e) => (_value.includes(e)))
|
|
94
|
+
}
|
|
95
|
+
if (typeof rowValue !== 'object') {
|
|
96
|
+
return !_value.includes(rowValue)
|
|
97
|
+
}
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
case '$range': {
|
|
101
|
+
const [min, max] = value['$range']
|
|
102
|
+
if (typeof min === 'number' && typeof max === 'number' && rowValue >= min && rowValue <= max) {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
getValidation
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
getValidation
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getValidation } from './getValidation'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function getValueByKeys(keys, data) {
|
|
2
|
+
if (keys.length === 0) {
|
|
3
|
+
return data
|
|
4
|
+
}
|
|
5
|
+
const firstKey = keys.shift()
|
|
6
|
+
if (data && Object.prototype.hasOwnProperty.call(data, firstKey)) {
|
|
7
|
+
return getValueByKeys(keys, data[firstKey])
|
|
8
|
+
}
|
|
9
|
+
if (data && firstKey) {
|
|
10
|
+
return data[firstKey]
|
|
11
|
+
}
|
|
12
|
+
return data
|
|
13
|
+
}
|
|
14
|
+
export default {
|
|
15
|
+
getValueByKeys
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
getValueByKeys
|
|
19
|
+
}
|
|
20
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getValueByKeys } from './getValueByKeys'
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// const { authenticator } = require('./authenticator')
|
|
2
|
+
// const { cryptoHelper } = require('./cryptoHelper')
|
|
3
|
+
// const { jwtHelper } = require('./jwtHelper')
|
|
4
|
+
|
|
5
|
+
// module.exports = {
|
|
6
|
+
// authenticator,
|
|
7
|
+
// cryptoHelper,
|
|
8
|
+
// jwtHelper
|
|
9
|
+
// }
|
|
10
|
+
|
|
11
|
+
export * from './convertString'
|
|
12
|
+
export * from './getValidation'
|
|
13
|
+
export * from './getValueByKeys'
|
|
14
|
+
export * from './keyValueObject'
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// 'use strict'
|
|
2
|
+
|
|
3
|
+
/* eslint-disable new-cap */
|
|
4
|
+
/* eslint-disable camelcase */
|
|
5
|
+
/* eslint-disable no-mixed-operators */
|
|
6
|
+
/* eslint-disable no-useless-escape */
|
|
7
|
+
/* eslint-disable no-param-reassign */
|
|
8
|
+
|
|
9
|
+
/* eslint func-names: 0 */
|
|
10
|
+
|
|
11
|
+
const Buffer = require('buffer/').Buffer
|
|
12
|
+
|
|
13
|
+
const EXPIRY = 3600 // in second
|
|
14
|
+
const ALGORITHM = 'HS256'
|
|
15
|
+
const SECRET = 'ab1234cd'
|
|
16
|
+
|
|
17
|
+
const jwtHelper = {
|
|
18
|
+
create(obj, { secret, algorithm, expiry } = {}) {
|
|
19
|
+
const sAlgorithm = algorithm || ALGORITHM
|
|
20
|
+
const sSecret = secret || SECRET
|
|
21
|
+
const exp = expiry || getTimeInSecond() + EXPIRY
|
|
22
|
+
const payload = {
|
|
23
|
+
...obj,
|
|
24
|
+
exp
|
|
25
|
+
}
|
|
26
|
+
return encode(payload, sSecret, sAlgorithm)
|
|
27
|
+
},
|
|
28
|
+
createByUser(loginAccount) {
|
|
29
|
+
const exp = getTimeInSecond() + EXPIRY
|
|
30
|
+
const payload = {
|
|
31
|
+
loginAccount,
|
|
32
|
+
exp
|
|
33
|
+
}
|
|
34
|
+
return this.encode(payload)
|
|
35
|
+
},
|
|
36
|
+
encode(payload, algorithm) {
|
|
37
|
+
return encode(payload, SECRET, algorithm)
|
|
38
|
+
},
|
|
39
|
+
decode(token, algorithm) {
|
|
40
|
+
const noVerify = !this.verify(token)
|
|
41
|
+
return decode(token, SECRET, noVerify, algorithm) // if noVerify = true, may skip verification
|
|
42
|
+
},
|
|
43
|
+
getPayload(token) {
|
|
44
|
+
const payload = getPayload(token)
|
|
45
|
+
return {
|
|
46
|
+
payload
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
getPayloadIdByKey(token, key) {
|
|
50
|
+
const payload = getPayload(token)
|
|
51
|
+
const id = payload[key] ? payload[key].id : null
|
|
52
|
+
return {
|
|
53
|
+
id,
|
|
54
|
+
jwtToken: token
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
resolve(token, secret, algorithm) {
|
|
58
|
+
const sSecret = secret || SECRET
|
|
59
|
+
return decode(token, sSecret, false, algorithm) // need verification
|
|
60
|
+
},
|
|
61
|
+
verify(token) {
|
|
62
|
+
const payload = getPayload(token)
|
|
63
|
+
const today = getTimeInSecond()
|
|
64
|
+
return (payload.exp && (today <= payload.exp)) || false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Private functions
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
const getPayload = (token) => decode(token, SECRET, true)
|
|
73
|
+
|
|
74
|
+
const getTimeInSecond = () => Math.floor(Date.now() / 1000)
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Private functions, based on jwt-simple 0.2.0
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
const { cryptoHelper } = require('../cryptoHelper')
|
|
81
|
+
|
|
82
|
+
const algorithmMap = {
|
|
83
|
+
HS256: 'sha256',
|
|
84
|
+
HS384: 'sha384',
|
|
85
|
+
HS512: 'sha512',
|
|
86
|
+
RS256: 'RSA-SHA256'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const typeMap = {
|
|
90
|
+
HS256: 'hmac',
|
|
91
|
+
HS384: 'hmac',
|
|
92
|
+
HS512: 'hmac',
|
|
93
|
+
RS256: 'sign'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decode jwt
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} token
|
|
101
|
+
* @param {String} key
|
|
102
|
+
* @param {Boolean} noVerify
|
|
103
|
+
* @param {String} algorithm
|
|
104
|
+
* @return {Object} payload
|
|
105
|
+
* @api public
|
|
106
|
+
*/
|
|
107
|
+
const decode = function jwt_decode(token, key, noVerify, algorithm) {
|
|
108
|
+
// check token
|
|
109
|
+
if (!token) {
|
|
110
|
+
throw new Error('No token supplied')
|
|
111
|
+
}
|
|
112
|
+
// check segments
|
|
113
|
+
const segments = token.split('.')
|
|
114
|
+
if (segments.length !== 3) {
|
|
115
|
+
throw new Error('Not enough or too many segments')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// All segment should be base64
|
|
119
|
+
const headerSeg = segments[0]
|
|
120
|
+
const payloadSeg = segments[1]
|
|
121
|
+
const signatureSeg = segments[2]
|
|
122
|
+
|
|
123
|
+
// base64 decode and parse JSON
|
|
124
|
+
const header = JSON.parse(base64urlDecode(headerSeg))
|
|
125
|
+
|
|
126
|
+
const payload = JSON.parse(base64urlDecode(payloadSeg))
|
|
127
|
+
|
|
128
|
+
if (!noVerify) {
|
|
129
|
+
const signingMethod = algorithmMap[algorithm || header.alg]
|
|
130
|
+
const signingType = typeMap[algorithm || header.alg]
|
|
131
|
+
if (!signingMethod || !signingType) {
|
|
132
|
+
throw new Error('Algorithm not supported')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// verify signature. `sign` will return base64 string.
|
|
136
|
+
const signingInput = [headerSeg, payloadSeg].join('.')
|
|
137
|
+
if (!verify(signingInput, key, signingMethod, signingType, signatureSeg)) {
|
|
138
|
+
throw new Error('Signature verification failed')
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return payload
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Encode jwt
|
|
148
|
+
*
|
|
149
|
+
* @param {Object} payload
|
|
150
|
+
* @param {String} key
|
|
151
|
+
* @param {String} algorithm
|
|
152
|
+
* @return {String} token
|
|
153
|
+
* @api public
|
|
154
|
+
*/
|
|
155
|
+
const encode = function jwt_encode(payload, key, algorithm) {
|
|
156
|
+
// Check key
|
|
157
|
+
if (!key) {
|
|
158
|
+
throw new Error('Require key')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check algorithm, default is HS256
|
|
162
|
+
if (!algorithm) {
|
|
163
|
+
algorithm = ALGORITHM
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const signingMethod = algorithmMap[algorithm]
|
|
167
|
+
const signingType = typeMap[algorithm]
|
|
168
|
+
if (!signingMethod || !signingType) {
|
|
169
|
+
throw new Error('Algorithm not supported')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// header, typ is fixed value.
|
|
173
|
+
const header = { typ: 'JWT', alg: algorithm }
|
|
174
|
+
|
|
175
|
+
// create segments, all segments should be base64 string
|
|
176
|
+
const segments = []
|
|
177
|
+
segments.push(base64urlEncode(JSON.stringify(header)))
|
|
178
|
+
segments.push(base64urlEncode(JSON.stringify(payload)))
|
|
179
|
+
segments.push(sign(segments.join('.'), key, signingMethod, signingType))
|
|
180
|
+
|
|
181
|
+
return segments.join('.')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* private util functions
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
function verify(input, key, method, type, signature) {
|
|
190
|
+
if (type === 'hmac') {
|
|
191
|
+
return (signature === sign(input, key, method, type))
|
|
192
|
+
} else if (type === 'sign') {
|
|
193
|
+
try {
|
|
194
|
+
return cryptoHelper.createStringVerify({
|
|
195
|
+
algorithm: method,
|
|
196
|
+
data: input,
|
|
197
|
+
object: key,
|
|
198
|
+
signature: base64urlUnescape(signature),
|
|
199
|
+
signatureEncoding: 'base64'
|
|
200
|
+
})
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw new Error('createStringVerify failed')
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
throw new Error('Algorithm type not recognized')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sign(input, key, method, type) {
|
|
209
|
+
let base64str
|
|
210
|
+
if (type === 'hmac') {
|
|
211
|
+
base64str = cryptoHelper.createStringHmac({
|
|
212
|
+
algorithm: method,
|
|
213
|
+
key,
|
|
214
|
+
data: input,
|
|
215
|
+
outputEncoding: 'base64'
|
|
216
|
+
})
|
|
217
|
+
} else if (type === 'sign') {
|
|
218
|
+
try {
|
|
219
|
+
base64str = cryptoHelper.createSignature({
|
|
220
|
+
algorithm: method,
|
|
221
|
+
data: input,
|
|
222
|
+
privateKey: key,
|
|
223
|
+
outputEncoding: 'base64'
|
|
224
|
+
})
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new Error('createSignature failed')
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
throw new Error('Algorithm type not recognized')
|
|
230
|
+
}
|
|
231
|
+
return base64urlEscape(base64str)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function base64urlDecode(str) {
|
|
235
|
+
// fixed bug if decode string is incorrect
|
|
236
|
+
return (new Buffer.from(base64urlUnescape(str), 'base64')).toString() || '{}'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function base64urlUnescape(str) {
|
|
240
|
+
str += new Array(5 - str.length % 4).join('=')
|
|
241
|
+
return str.replace(/\-/g, '+').replace(/_/g, '/')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function base64urlEncode(str) {
|
|
245
|
+
return base64urlEscape(new Buffer.from((str)).toString('base64'))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function base64urlEscape(str) {
|
|
249
|
+
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
jwtHelper
|
|
254
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { KeyValueObject } from './keyValueObject'
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
class KeyValueObject {
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
options = options || {}
|
|
6
|
+
this.key = options.key || null
|
|
7
|
+
this.value = (typeof options.value !== 'undefined') ? options.value : ''
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Class methods
|
|
11
|
+
|
|
12
|
+
static addItem(arr, key, value) {
|
|
13
|
+
arr.push(
|
|
14
|
+
{ key, value }
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
static addRecord(arr = [], key, value) {
|
|
18
|
+
const self = this
|
|
19
|
+
if (!KeyValueObject.hasKeyValue(arr, key, value)) {
|
|
20
|
+
arr.push(self.init({ key, value }))
|
|
21
|
+
}
|
|
22
|
+
return arr
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static fromObject(options = {}) {
|
|
26
|
+
const self = this
|
|
27
|
+
return Object.keys(options).reduce((acc, key) => {
|
|
28
|
+
acc.push(self.init({ key, value: options[key] }))
|
|
29
|
+
return acc
|
|
30
|
+
}, [])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static removeByKey(arr, key) {
|
|
34
|
+
return arr.reduce((acc, item) => {
|
|
35
|
+
if (item.key !== key) {
|
|
36
|
+
acc.push(item)
|
|
37
|
+
}
|
|
38
|
+
return acc
|
|
39
|
+
}, [])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
static getValuesByKey(arr = [], key) {
|
|
43
|
+
return arr.reduce((acc, item) => {
|
|
44
|
+
if (item.key === key) {
|
|
45
|
+
acc.push(item.value)
|
|
46
|
+
}
|
|
47
|
+
return acc
|
|
48
|
+
}, [])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static hasKeyValue(arr = [], key, value) {
|
|
52
|
+
if (typeof value === 'undefined') {
|
|
53
|
+
return arr.filter((item) => item.key === key).length > 0
|
|
54
|
+
}
|
|
55
|
+
return arr.filter((item) => (item.key === key && item.value === value)).length > 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static init(options = {}) {
|
|
59
|
+
if (options instanceof KeyValueObject) {
|
|
60
|
+
return options
|
|
61
|
+
}
|
|
62
|
+
const kvObject = new KeyValueObject(options)
|
|
63
|
+
return kvObject.isValid ? kvObject : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static initFromArray(arr = []) {
|
|
67
|
+
const self = this
|
|
68
|
+
if (Array.isArray(arr)) {
|
|
69
|
+
return arr.map(self.init)
|
|
70
|
+
}
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static initOnlyValidFromArray(arr = []) {
|
|
75
|
+
return this.initFromArray(arr).filter((i) => i)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static keys(arr = []) {
|
|
79
|
+
if (Array.isArray(arr)) {
|
|
80
|
+
return arr.reduce((acc, item) => {
|
|
81
|
+
acc.push(item.key)
|
|
82
|
+
return acc
|
|
83
|
+
}, [])
|
|
84
|
+
}
|
|
85
|
+
return []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static toObject(arr = []) {
|
|
89
|
+
if (Array.isArray(arr)) {
|
|
90
|
+
return arr.reduce((acc, item) => {
|
|
91
|
+
acc[item.key] = item.value
|
|
92
|
+
return acc
|
|
93
|
+
}, {})
|
|
94
|
+
}
|
|
95
|
+
return {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static toString(arr = [], delimiter = '; ') {
|
|
99
|
+
if (Array.isArray(arr)) {
|
|
100
|
+
return arr.reduce((acc, item) => {
|
|
101
|
+
acc.push(`${item.key}: ${item.value}`)
|
|
102
|
+
return acc
|
|
103
|
+
}, []).join(delimiter)
|
|
104
|
+
}
|
|
105
|
+
return ''
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static updateRecord(arr = [], key, value) {
|
|
109
|
+
return arr.map((item) => {
|
|
110
|
+
if (item.key === key) {
|
|
111
|
+
item.value = value
|
|
112
|
+
}
|
|
113
|
+
return item
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static updateOrInsertRecord(arr = [], key, value) {
|
|
118
|
+
const self = this
|
|
119
|
+
let copy = [...arr]
|
|
120
|
+
if (!self.hasKeyValue(arr, key)) {
|
|
121
|
+
copy.push(self.init({ key, value }))
|
|
122
|
+
} else {
|
|
123
|
+
copy = self.updateRecord(arr, key, value)
|
|
124
|
+
}
|
|
125
|
+
return copy
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static updateRecordsFromArray(arr = [], updateArr = []) {
|
|
129
|
+
if (Array.isArray(arr) && Array.isArray(updateArr)) {
|
|
130
|
+
const obj1 = KeyValueObject.toObject(arr)
|
|
131
|
+
const obj2 = KeyValueObject.toObject(updateArr)
|
|
132
|
+
return KeyValueObject.fromObject(Object.assign({}, obj1, obj2))
|
|
133
|
+
}
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static values(arr = []) {
|
|
138
|
+
if (Array.isArray(arr)) {
|
|
139
|
+
return arr.reduce((acc, item) => {
|
|
140
|
+
acc.push(item.value)
|
|
141
|
+
return acc
|
|
142
|
+
}, [])
|
|
143
|
+
}
|
|
144
|
+
return []
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// getters
|
|
148
|
+
get isValid() {
|
|
149
|
+
return !!this.key
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
get toObject() {
|
|
153
|
+
const obj = {}
|
|
154
|
+
if (this.isValid) {
|
|
155
|
+
obj[this.key] = this.value
|
|
156
|
+
}
|
|
157
|
+
return obj
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
KeyValueObject
|
|
163
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@questwork/q-utilities",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Questwork QUtilities",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Questwork Consulting Limited",
|
|
8
|
+
"email": "info@questwork.com",
|
|
9
|
+
"url": "https://questwork.com/"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"axios": "^0.28.1",
|
|
14
|
+
"buffer": "^6.0.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"chai": "^4.2.0",
|
|
18
|
+
"eslint": "4.0.0",
|
|
19
|
+
"eslint-config-airbnb-base": "11.2.0",
|
|
20
|
+
"eslint-plugin-import": "2.3.0",
|
|
21
|
+
"eslint-plugin-mocha": "^5.3.0",
|
|
22
|
+
"mocha": "^6.2.2",
|
|
23
|
+
"nodemon": "^2.0.2"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=10.0.0"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"lint": "eslint .",
|
|
30
|
+
"test": "mocha --exit 'test/test.setup.js' 'test/**/*.spec.js'",
|
|
31
|
+
"test-models": "NODE_ENV=test mocha --exit 'test/test.setup.js' 'test/**/*.spec.js'"
|
|
32
|
+
}
|
|
33
|
+
}
|